import { Col, colUnwrap, colWrap, ifNull, numberCol } from "./Column"; import { compQuery, finalCols, freshScope } from "./Compile"; import { Exp, SomeCol } from "./Exp"; import { GenState, rename } from "./GenState"; import { freshName, isolate, Query, queryBind, queryPure } from "./Query/Type"; import { JoinType, Order, SQL, sqlFrom } from "./SQL"; import { SqlType } from "./SqlType"; import * as State from "./StateMonad"; import { Table } from "./Table"; import { allCols, colNames, state2sql } from "./Transform"; import { ColName, TableName } from "./Types"; import { Unsafe } from "./Unsafe"; function mkSome(val: Exp, parser: (val: string) => a): SomeCol { return { type: "Some", exp: val, parser: parser }; } function mkCol(val: ColName, parser: (val: string) => a): Exp { return { type: "ECol", correlation: null, colName: val, parser: parser }; } /** * The type of [[defaultValue]]. You cannot instantiate values of this. */ export class DefaultValue { protected dummy: DefaultValue; } const defValue: DefaultValue = {}; /** * Can be used in `insert` and `update` operations, to set the value * of a column to its default value. * * This can only be used on table columns that have been declared as * "default-able" when the table was declared using [[declareTable]]. * * This is like using the `DEFAULT` keyword in SQL. */ export function defaultValue(): DefaultValue { return defValue; } export type MakeCols = { [P in keyof T]: Col; }; export class Write { protected dummy: Write; } export type MakeTable = { [P in keyof T1]: Col; } & { [P in keyof T2]: Col | DefaultValue; }; export function select(table: Table): Query> { const cs2 = table.tableCols.map<[ColName, string, (val: string) => any]>(c => [c.name, c.propName, c.parser]); const result: Query> = new Query(resolve => { resolve( State.bind ( State.mapM(x => State.bind, [SomeCol, string, (val: string) => any]>(rename(mkSome(mkCol(x[0], x[2]), x[2])), y => State.pure, string, (val: string) => any]>([y, x[1], x[2]])), cs2), (rns: [SomeCol, string, (val: string) => any][]) => State.bind ( State.get(), st => { const newSource = sqlFrom(rns.map(x => x[0]), { type: "TableName", tableName: table.tableName }); const st2: GenState = { ...st, sources: [newSource].concat(st.sources) }; return State.bind ( State.put(st2), () => State.pure(toTup(someColNames2(rns), null)) ); } ) )); }); return result; } /** * Query an ad hoc table. Each element in the given list represents one row * in the ad hoc table. */ export function selectValues(vals: MakeCols[]): Query> { if (vals.length === 0) { const result: Query> = new Query(resolve => { resolve( State.bind( State.get(), st => { const s2 = sqlFrom([], { type: "EmptyTable" }); return State.bind( State.put({ ...st, sources: [s2].concat(st.sources) }), () => State.pure({}) ); } ) ); }); return result; } const row = vals[0]; const rows = vals.slice(1); const firstrow = finalCols(row); const mkFirstRow = (ns: ColName[]): SomeCol[] => { const results: SomeCol[] = []; for (let i = 0; i < firstrow.length; ++i) { results.push({ type: "Named", colName: ns[i], exp: firstrow[i].exp, parser: firstrow[i].parser, propName: (firstrow[i]).propName }); } return results; }; const rows2 = rows.map(finalCols); const result: Query> = new Query(resolve => { resolve( State.bind( State.mapM(() => freshName(), firstrow), names => { const rns: SomeCol[] = []; let i = 0; for (const n of names) { rns.push({ type: "Named", colName: n, exp: { type: "ECol", correlation: null, colName: n, parser: () => { throw new Error("ECol parser"); } }, parser: (firstrow[i]).parser, propName: (firstrow[i]).propName }); i++; } const row2 = mkFirstRow(names); return State.bind( State.get(), s => { const s2 = sqlFrom(rns, { type: "Values", cols: row2, params: rows2 }); return State.bind( State.put({ ...s, sources: [s2].concat(s.sources) }), () => { const ts: [ColName, string, (val: string) => any][] = []; for (const r of rns) { ts.push([(>r).colName, (>r).propName, r.parser]); } return State.pure(toTup(ts, null)); } ); } ); } )); }); return result; } export function restrict(expr: Col): Query { const result: Query = new Query(resolve => { resolve( State.bind(State.get(), st => State.put((() => { if (st.sources.length === 0) { return { ...st, staticRestricts: [colUnwrap(expr)].concat(st.staticRestricts) }; } else if (st.sources.length === 1 && !wasRenamedIn(colUnwrap(expr), st.sources[0].cols)) { return { ...st, sources: [{ ...st.sources[0], restricts: [colUnwrap(expr)].concat(st.sources[0].restricts) }] }; } else { const source2 = sqlFrom(allCols(st.sources), { type: "Product", sqls: st.sources }); return { ...st, sources: [{ ...source2, restricts: [colUnwrap(expr)] }] }; } })())) ); }); return result; } export function aggregate(q: Query, AggrCols>): Query> { return new Query(resolve => { resolve( State.bind( isolate(q), is => { const [gst, aggrs] = is; return State.bind( State.mapM(x => State.bind(rename(x[0]), y => State.pure, string, (val: string) => any]>([y, x[1], x[2]])), fromTup(aggrs)), (cs: [SomeCol, string, (val: string) => any][]) => { const sql: SQL = { ...sqlFrom(cs.map(x => x[0]), { type: "Product", sqls: [state2sql(gst)] }), groups: gst.groupCols }; return State.bind( State.modify(st => ({ ...st, sources: [sql].concat(st.sources) })), () => State.pure(toTup(someColNames2(cs), null)) ); } ); } ) ); }); } export function groupBy(col: Col, a>): Query, Aggr, a>> { const g: SomeCol[] = [{ type: "Some", exp: colUnwrap(col), parser: null // TODO Check that this is corect }]; return new Query(resolve => { resolve( State.bind( State.get(), st => State.bind( State.put({ ...st, groupCols: g.concat(st.groupCols) }), () => State.pure(colUnwrap(col)) ) ) ); }); } export function limit(from: number, to: number, q: Query, MakeCols, a>>): Query> { return new Query(resolve => { resolve( State.bind( isolate(q), is => { const [lim_st, res] = is; return State.bind( State.get(), st => { let sql: SQL; if (lim_st.sources.length === 1 && lim_st.sources[0].limits === null) { sql = lim_st.sources[0]; } else { sql = sqlFrom(allCols(lim_st.sources), { type: "Product", sqls: lim_st.sources }); } const sql2: SQL = { ...sql, limits: [from, to] }; return State.bind( State.put({ ...st, sources: [sql2].concat(st.sources) }), () => State.pure(res) ); } ); } ) ); }); } export function order(col: Col, order: Order): Query { return new Query(resolve => { resolve( State.bind( State.get(), st => { const newOrder: [Order, SomeCol] = [order, { type: "Some", exp: colUnwrap(col), parser: (_val: string) => { throw new Error("TODO"); } }]; const sql = sqlFrom(allCols(st.sources), { type: "Product", sqls: st.sources }); if (st.sources.length === 1) { return State.put({ ...st, sources: [{ ...sql, ordering: [newOrder].concat(sql.ordering) }] }); } else { return State.put({ ...st, sources: [{ ...sql, ordering: [newOrder] }] }); } }) ); }); } export function distinct(quer: Query): Query { return new Query(resolve => { resolve( State.bind( isolate(quer), i => { const [inner_st, res] = i; return State.bind( State.get(), st => State.bind( (State.put({ ...st, sources: [{ ...sqlFrom(allCols(inner_st.sources), { type: "Product", sqls: inner_st.sources }), distinct: true }] })), () => State.pure(res) ) ); } ) ); }); } export function count(col: Col): Aggr { return { type: "EAggrEx", name: "COUNT", exp: colUnwrap(col), parser: SqlType.numberParser }; } export function avg(col: Col): Aggr { return { type: "EAggrEx", name: "AVG", exp: colUnwrap(col), parser: SqlType.numberParser }; } export function sum(col: Col): Aggr { return colUnwrap( ifNull( Unsafe.unsafeCast(numberCol(0), "INT", SqlType.intParser), colWrap({ type: "EAggrEx", name: "SUM", exp: colUnwrap(col), parser: SqlType.numberParser }) ) ); } export function max(col: Col): Aggr { return { type: "EAggrEx", name: "MAX", exp: colUnwrap(col), parser: (colUnwrap(col)).parser }; } export function min(col: Col): Aggr { return { type: "EAggrEx", name: "MIN", exp: colUnwrap(col), parser: (colUnwrap(col)).parser }; } export function inList(lhs: Col, rhs: Col[]): Col { if (rhs.length === 0) { return colWrap({ type: "ELit", lit: { type: "LBool", value: false }, parser: SqlType.booleanParser }); } else { return colWrap({ type: "EInList", exp: colUnwrap(lhs), exps: rhs, // Dangerous but safe! parser: SqlType.booleanParser }); } } export function inQuery(lhs: Col, rhs: Query>): Col { const q2 = queryBind(rhs, x => queryPure({ val: x })); // Column name can be anything, just need to make sure there is only one return colWrap({ type: "EInQuery", exp: colUnwrap(lhs), sql: compQuery(freshScope(), q2), parser: SqlType.booleanParser }); } function wasRenamedIn(predicate: Exp, cs: SomeCol[]): boolean { const cs2 = someColNames(cs); return colNames([{ type: "Some", exp: predicate, parser: null }]).find(colName => cs2.indexOf(colName) >= 0) !== undefined; } function someColNames(someCols: SomeCol[]): ColName[] { const results: ColName[] = []; for (const someCol of someCols) { if (someCol.type === "Named") { results.push(someCol.colName); } } return results; } function someColNames2(someCols: [SomeCol, string, (val: string) => any][]): [ColName, string, (val: string) => any][] { const results: [ColName, string, (val: string) => any][] = []; for (const s of someCols) { const [someCol, propName, parser] = s; if (someCol.type === "Named") { results.push([someCol.colName, propName, parser]); } } return results; } export class Inner { protected dummy: [Inner, s]; } // This is really Just a Col export class Aggr { protected dummy: [Aggr, s, a]; } export type LeftCols = { [P in keyof A]: Col; }; export type AggrCols = { [P in keyof A]: Aggr, A[P]>; }; export function leftJoin(s: Query, MakeCols, a>>, pred: (p: MakeCols) => Col): Query> { return someJoin(JoinType.LeftJoin, pred, s); } export function innerJoin(s: Query, MakeCols, a>>, pred: (p: MakeCols) => Col): Query> { return someJoin(JoinType.InnerJoin, pred, s); } /** * The actual code for any join. */ function someJoin(jointype: JoinType, check: any, q: Query, MakeCols, a>>): Query { const s: State.State = State.bind( isolate(q), is => { const [join_st, res] = is; // tslint:disable-line:variable-name return State.bind( State.mapM(x => State.bind(rename(x[0]), y => State.pure, string, (val: string) => any]>([y, x[1], x[2]])), fromTup(res)), cs => State.bind( State.get(), st => { const nameds = someColNames2(cs); const left = state2sql(st); const right = sqlFrom(cs.map(x => x[0]), { type: "Product", sqls: [state2sql(join_st)] }); const on: Col = check(toTup(nameds, null)); let outCols: SomeCol[] = []; for (const c of cs) { const c2 = c[0]; if (c2.type === "Named") { outCols.push({ type: "Some", exp: { type: "ECol", correlation: null, colName: c2.colName, parser: c2.parser }, parser: c2.parser }); } } outCols = outCols.concat(allCols([left])); return State.bind( State.put({ ...st, sources: [sqlFrom(outCols, { type: "Join", joinType: jointype, exp: colUnwrap(on), left: left, right: right })] }), () => State.pure(toTup(nameds, null)) ); } ) ); } ); return new Query(resolve => { resolve(s); }); } function fromTup(c: MakeCols): [SomeCol, string, (val: string) => any][] { const keys = Object.keys(c); const result: [SomeCol, string, (val: string) => any][] = []; for (const key of keys) { const col: Col = (c)[key]; const exp = colUnwrap(col); result.push([{ type: "Some", exp: exp, parser: exp.parser }, key, exp.parser]); // TODO we might need to set propName here } return result; } export function toTup(colNames: [ColName, string, (val: string) => any][], correlation: TableName | null): a { const results: any = {}; for (const c of colNames) { const [colName, propName, parser] = c; results[propName] = colWrap({ type: "ECol", correlation: correlation, colName: colName, parser: parser }); } return results; }