import { booleanCol, Col, colUnwrap, colWrap, numberCol } from "./Column"; import { compQuery2, freshScope, resetGlobalNameSupply, resetScope } from "./Compile"; import * as Debug from "./Debug"; import { QueryMetricsImpl } from "./Frontend"; import * as Frontend from "./Frontend"; import { GenState, initState } from "./GenState"; import { pg } from "./pg"; import * as m from "./Query"; import { Aggr, AggrCols, Inner, LeftCols, MakeCols } from "./Query"; import { Order } from "./SQL"; import { SqlType } from "./SqlType"; import { StreamingRows } from "./StreamingRows"; import { Table } from "./Table"; import { Unsafe } from "./Unsafe"; export class Q { protected dummy: [Q, s]; } // Single element (a simple box) type MutQuery = GenState[]; /** * @param sqlTag Will be injected as a comment into the SQL that is sent to the server. Useful for identifying the query during log analysis and performance analysis */ export async function query(sqlTag: string | undefined, conn: pg.Client, q: (q: Q<{}>) => MakeCols<{}, t>): Promise { if (Debug.enabled) { Debug.lastQueryMetrics.set(conn, new QueryMetricsImpl()); // tslint:disable-next-line:no-non-null-assertion (Debug.lastQueryMetrics.get(conn)!).setStage1BeforeCompileQuery(); } // This ensures that the generated SQL will be the same for identical queries resetScope(); resetGlobalNameSupply(); const mutQ: MutQuery = [initState(0)]; const result = q(mutQ); const sql = compQuery2(result, mutQ[0]); return Frontend.query2(sqlTag, conn, sql); } /** * Perform a query, but stream the results rather than loading them all into * memory. * * After you call this function, you *must* call the `readAllRows` function, and * you must call it while the connection is still open (and don't close the * connection until it completes). * * @param sqlTag Will be injected as a comment into the SQL that is sent to the server. Useful for identifying the query during log analysis and performance analysis * @param rowChunkSize How many rows to read and process during each iteration */ export async function queryStreaming(sqlTag: string | undefined, conn: pg.Client, q: (q: Q<{}>) => MakeCols<{}, t>, rowChunkSize = 2000): Promise> { // This ensures that the generated SQL will be the same for identical queries resetScope(); resetGlobalNameSupply(); const mutQ: MutQuery = [initState(0)]; const result = q(mutQ); const sql = compQuery2(result, mutQ[0]); return Frontend.query2Streaming(sqlTag, conn, sql, rowChunkSize); } export function select(q: Q, table: Table): MakeCols { const mutQ: MutQuery = q; const [x, y] = m.select(table).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } /** * Query an ad hoc table. Each element in the given list represents one row * in the ad hoc table. */ export function selectValues(q: Q, vals: MakeCols[]): MakeCols { const mutQ: MutQuery = q; const [x, y] = m.selectValues(vals).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function restrict(q: Q, expr: Col): void { const mutQ: MutQuery = q; const [x, y] = m.restrict(expr).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function leftJoin(q: Q, s: (q: Q>) => MakeCols, a>, pred: (p: MakeCols) => Col): LeftCols { const mutQ: MutQuery = q; const qr = { unQ: { runState: (x: GenState): [a, GenState] => { const mutQ2: MutQuery = [x]; const result = s(mutQ2); return [result, mutQ2[0]]; } } }; const [x, y] = m.leftJoin(qr, pred).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function innerJoin(q: Q, s: (q: Q>) => MakeCols, a>, pred: (p: MakeCols) => Col): MakeCols { const mutQ: MutQuery = q; const qr = { unQ: { runState: (x: GenState): [a, GenState] => { const mutQ2: MutQuery = [x]; const result = s(mutQ2); return [result, mutQ2[0]]; } } }; const [x, y] = m.innerJoin(qr, pred).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } /** * Explicitly create an inner query. * * Sometimes it's handy, for performance reasons and otherwise, to perform a * subquery and restrict only that query before adding the result of the * query to the result set, instead of first adding the query to the result * set and restricting the whole result set afterwards. */ export function inner(q: Q, query: (q: Q>) => MakeCols, a>): MakeCols { return innerJoin(q, query, () => booleanCol(true)); } /** * Create and filter an inner query, before adding it to the current result * set. * * `suchThat(q, query, p)` * is generally more efficient than * `const x = query(q); restrict(pred(x)); return x;` */ export function suchThat(q: Q, query: (q: Q>) => MakeCols, a>, pred: (row: MakeCols, a>) => Col, boolean>): MakeCols { return inner(q, q => { const x = query(q); restrict(q, pred(x)); return x; }); } export function aggregate(q: Q, s: (q: Q>) => AggrCols): MakeCols { const mutQ: MutQuery = q; const qr = { unQ: { runState: (x: GenState): [a, GenState] => { const mutQ2: MutQuery = [x]; const result = s(mutQ2); return [result, mutQ2[0]]; } } }; const [x, y] = m.aggregate(qr).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function groupBy(q: Q>, col: Col, a>): Aggr, a> { const mutQ: MutQuery = q; const [x, y] = m.groupBy(col).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function inQuery(lhs: Col, rhs: (q: Q) => Col): Col { const mutQ: MutQuery = [initState(freshScope())]; const result = rhs(mutQ); const result2 = { val: result }; // Column name can be anything, just need to make sure there is only one return colWrap({ type: "EInQuery", exp: colUnwrap(lhs), sql: compQuery2(result2, mutQ[0]), parser: SqlType.booleanParser }); } /** * The type of an [[arbitrary]] column */ export class Arbitrary { protected dummy: Arbitrary; } /** * An arbitrary column, useful for queries where the values of the returned * rows are not important, such as in [[exists]] queries. */ export function arbitrary(): Col { return Unsafe.unsafeCast(numberCol(1), "INT", SqlType.intParser); } /** * Does the subquery have at least one row? * * SQL equivalent: EXISTS * * @param subquery The subquery should return [[arbitrary]] (since the values, * of the resulting rows in unimportant). */ export function exists(subquery: (q: Q) => Col): Col { const mutQ: MutQuery = [initState(freshScope())]; const result = subquery(mutQ); const result2 = { val: result }; // Column name can be anything, just need to make sure there is only one return colWrap({ type: "EExists", sql: compQuery2(result2, mutQ[0]), parser: SqlType.booleanParser }); } export function limit(q: Q, from: number, to: number, query: (q: Q>) => MakeCols, a>): MakeCols { const mutQ: MutQuery = q; const qr = { unQ: { runState: (x: GenState): [a, GenState] => { const mutQ2: MutQuery = [x]; const result = query(mutQ2); return [result, mutQ2[0]]; } } }; const [x, y] = m.limit(from, to, qr).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function order(q: Q, col: Col, order: Order): void { const mutQ: MutQuery = q; const [x, y] = m.order(col, order).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } export function distinct(q: Q, query: (q: Q) => MakeCols): MakeCols { const mutQ: MutQuery = q; const qr = { unQ: { runState: (x: GenState): [a, GenState] => { const mutQ2: MutQuery = [x]; const result = query(mutQ2); return [result, mutQ2[0]]; } } }; const [x, y] = m.distinct(qr).unQ.runState(mutQ[0]); mutQ[0] = y; return x; } /** * Similar to [[query]], but when you are certain that the query will always return * exactly 1 row. * * For example: COUNT(*) style queries * * @param sqlTag Will be injected as a comment into the SQL that is sent to the server. Useful for identifying the query during log analysis and performance analysis */ export async function queryOne(sqlTag: string | undefined, conn: pg.Client, q: (q: Q<{}>) => MakeCols<{}, t>): Promise { const rows = await query(sqlTag, conn, q); if (rows.length !== 1) { throw new Error(`Expected query to return 1 row, but got ${rows.length}`); } return rows[0]; } /** * Similar to [[query]], but when you are certain that the query will always return * either 1 row or 0 rows. * * For example: queries that are restricted on some primary key * * @param sqlTag Will be injected as a comment into the SQL that is sent to the server. Useful for identifying the query during log analysis and performance analysis */ export async function queryOneOrNone(sqlTag: string | undefined, conn: pg.Client, q: (q: Q<{}>) => MakeCols<{}, t>): Promise { const rows = await query(sqlTag, conn, q); if (rows.length === 0) { return null; } if (rows.length === 1) { return rows[0]; } throw new Error(`Expected query to return 1 or 0 rows, but got ${rows.length}`); }