diff --git a/packages/backend/src/execution/executors/PgExecutor/PgExecutor.test.ts b/packages/backend/src/execution/executors/PgExecutor/PgExecutor.test.ts index f9609860..ba6fc35f 100644 --- a/packages/backend/src/execution/executors/PgExecutor/PgExecutor.test.ts +++ b/packages/backend/src/execution/executors/PgExecutor/PgExecutor.test.ts @@ -48,6 +48,29 @@ describe('PgExecutor', () => { `); }); + it('.count() ', async () => { + const q = from('film') + .where({ + film_id: { in: [1, 2, 3] }, + }) + .groupBy() + .count(); + const { sql } = executor.compile(q); + + expect(sql).toMatchInlineSnapshot(` + "select + count(1) as "count" + from + "public"."film" "public::film" + where + ("public::film".film_id = any ($1))" + `); + + const result = await executor.execute(q, executeProps); + + expect(result).toEqual([{ count: 3 }]); + }); + it('Film table SynthQL query executes to expected result', async () => { const result = await executor.execute(q1, executeProps); diff --git a/packages/backend/src/execution/executors/PgExecutor/PgExecutor.ts b/packages/backend/src/execution/executors/PgExecutor/PgExecutor.ts index c25fb6c7..93a391a2 100644 --- a/packages/backend/src/execution/executors/PgExecutor/PgExecutor.ts +++ b/packages/backend/src/execution/executors/PgExecutor/PgExecutor.ts @@ -1,8 +1,6 @@ import { Pool, PoolClient } from 'pg'; import { format } from 'sql-formatter'; import { splitQueryAtBoundary } from '../../../query/splitQueryAtBoundary'; -import { ColumnRef } from '../../../refs/ColumnRef'; -import { RefContext, createRefContext } from '../../../refs/RefContext'; import { AnyQuery } from '../../../types'; import { QueryExecutor } from '../../types'; import { QueryProviderExecutor } from '../QueryProviderExecutor'; diff --git a/packages/backend/src/execution/executors/PgExecutor/queryBuilder/AggregationsSelection.ts b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/AggregationsSelection.ts new file mode 100644 index 00000000..5fc76d2c --- /dev/null +++ b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/AggregationsSelection.ts @@ -0,0 +1,40 @@ +import { AnyQuery } from '../../../../types'; +import { Selection } from './types'; +import { SqlBuilder } from './exp'; + +// TODO(fhur): this is actually only supporting count(*) for now. +export class AggregationsSelection implements Selection { + public constructor() {} + + static fromQuery( + rootQuery: AnyQuery, + defaultSchema: string, + ): AggregationsSelection[] { + if (rootQuery.aggregates === undefined) { + return []; + } + if (Object.keys(rootQuery.aggregates).length === 0) { + return []; + } + return [new AggregationsSelection()]; + } + + toSql() { + return new SqlBuilder().addAs(['as', ['fn', 'count', '1'], 'count']); + } + + extractFromRow(row: any, target: any): void { + // TODO assert prescence + const tmp = row['count']; + if (typeof tmp !== 'string') { + throw new Error( + `Expected count to be a string, got: ${JSON.stringify(tmp)}`, + ); + } + + // Note technically correct, but we're not going to support counting bigints + // this is only an issue if you return more than Integer.MAX_VALUE rows, + // which is unlikely. + target['count'] = parseInt(tmp); + } +} diff --git a/packages/backend/src/execution/executors/PgExecutor/queryBuilder/createAugmentedQuery.ts b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/createAugmentedQuery.ts index 60ec80f6..8f8dc71f 100644 --- a/packages/backend/src/execution/executors/PgExecutor/queryBuilder/createAugmentedQuery.ts +++ b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/createAugmentedQuery.ts @@ -6,6 +6,7 @@ import { SelectionJsonbAgg } from './SelectionJsonbAgg'; import { TableRef } from '../../../../refs/TableRef'; import { ColumnRef } from '../../../../refs/ColumnRef'; import { Join, Selection } from './types'; +import { AggregationsSelection } from './AggregationsSelection'; export interface AugmentedQuery { selection: Selection[]; @@ -29,13 +30,17 @@ export function createAugmentedQuery( rootQuery, defaultSchema, ); + const aggregates = AggregationsSelection.fromQuery( + rootQuery, + defaultSchema, + ); const wheres = collectWhere(rootQuery, defaultSchema); const joins = collectJoins(rootQuery, defaultSchema); const groupingColumns = rootQuery.groupBy?.map((col) => rootTable.column(col)) ?? []; return { - selection: selectionColumn.concat(jsonbAggColumn), + selection: selectionColumn.concat(jsonbAggColumn).concat(aggregates), rootQuery, rootTable, joins, diff --git a/packages/backend/src/execution/executors/PgExecutor/queryBuilder/exp.ts b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/exp.ts index c84d29a9..844a561a 100644 --- a/packages/backend/src/execution/executors/PgExecutor/queryBuilder/exp.ts +++ b/packages/backend/src/execution/executors/PgExecutor/queryBuilder/exp.ts @@ -229,7 +229,6 @@ export class SqlBuilder { } addOperator(op: BinaryOperator) { - const unknownOp = op as unknown; if (!OPERATORS.includes(op as any)) { throw new Error(`Invalid operator: ${op}`); } diff --git a/packages/backend/src/tests/e2e/nx1.test.ts b/packages/backend/src/tests/e2e/nx1.test.ts index c4fe355b..c3cb8bc8 100644 --- a/packages/backend/src/tests/e2e/nx1.test.ts +++ b/packages/backend/src/tests/e2e/nx1.test.ts @@ -1,4 +1,4 @@ -import { Where, col } from '@synthql/queries'; +import { QueryResult, Where, col } from '@synthql/queries'; import { describe, expect, test } from 'vitest'; import { collectLast } from '../..'; import { execute } from '../../execution/execute'; @@ -166,4 +166,10 @@ describe('n x 1', () => { expect(c.expected).toEqual(c.expected); }); }); + + test('x', async () => { + const q = from('film').count(); + + type x = QueryResult; + }); }); diff --git a/packages/backend/src/tests/propertyBased/arbitraries/ArbitraryQueryBuilder.ts b/packages/backend/src/tests/propertyBased/arbitraries/ArbitraryQueryBuilder.ts index 98c0d189..eeb0bba9 100644 --- a/packages/backend/src/tests/propertyBased/arbitraries/ArbitraryQueryBuilder.ts +++ b/packages/backend/src/tests/propertyBased/arbitraries/ArbitraryQueryBuilder.ts @@ -19,6 +19,8 @@ export class ArbitraryQueryBuilder { private cardinalities: Cardinality[] = ['many', 'maybe', 'one'], private tables: Table[] = getTableNames(schema) as Table[], private hasResults: boolean = true, + private groupBy?: string[], + private skipLimit?: boolean, ) {} /** @@ -34,6 +36,19 @@ export class ArbitraryQueryBuilder { cardinality, this.tables, this.hasResults, + this.groupBy, + this.skipLimit, + ); + } + + withGroupBy(...groupBy: string[]) { + return new ArbitraryQueryBuilder( + this.schema, + this.cardinalities, + this.tables, + this.hasResults, + groupBy, + this.skipLimit, ); } @@ -43,6 +58,8 @@ export class ArbitraryQueryBuilder { this.cardinalities, tables, this.hasResults, + this.groupBy, + this.skipLimit, ); } @@ -57,6 +74,8 @@ export class ArbitraryQueryBuilder { this.cardinalities, this.tables, false, + this.groupBy, + this.skipLimit, ); } @@ -71,6 +90,19 @@ export class ArbitraryQueryBuilder { this.cardinalities, this.tables, true, + this.groupBy, + this.skipLimit, + ); + } + + withNoLimit() { + return new ArbitraryQueryBuilder( + this.schema, + this.cardinalities, + this.tables, + this.hasResults, + this.groupBy, + true, ); } @@ -115,14 +147,20 @@ export class ArbitraryQueryBuilder { } private arbGroupBy(tableName: Table): fc.Arbitrary { - return fc.constant(getPrimaryKeyColumns(this.schema, tableName)); + if (this.groupBy === undefined) { + return fc.constant(getPrimaryKeyColumns(this.schema, tableName)); + } + return fc.constant(this.groupBy); } private arbCardinality(): fc.Arbitrary { return fc.constantFrom(...this.cardinalities); } - private arbLimit(): fc.Arbitrary { + private arbLimit(): fc.Arbitrary { + if (this.skipLimit) { + return fc.constant(undefined); + } if (!this.hasResults) { return fc.constant(0); } diff --git a/packages/backend/src/tests/propertyBased/properties/count.test.ts b/packages/backend/src/tests/propertyBased/properties/count.test.ts new file mode 100644 index 00000000..61dcfa37 --- /dev/null +++ b/packages/backend/src/tests/propertyBased/properties/count.test.ts @@ -0,0 +1,60 @@ +import { test } from '@fast-check/vitest'; +import { ArbitraryQueryBuilder } from '../arbitraries/ArbitraryQueryBuilder'; +import { queryEngine } from '../../queryEngine'; +import { expect } from 'vitest'; +import { Query } from '@synthql/queries'; +import { DB } from '../../generated'; + +const queryBuilder = ArbitraryQueryBuilder.fromPagila(); + +const numRuns = 100; +const timeout = numRuns * 1000; +const endOnFailure = true; + +test.prop( + [ + queryBuilder + .withCardinality('many') + .withNoLimit() + .withSomeResults() + .build(), + ], + { + verbose: true, + numRuns, + timeout, + endOnFailure, + seed: 1308343585, + path: '2', + }, +)( + 'execute(query).length should equal execute(query.count()).count', + async (query) => { + const queryResult = (await queryEngine.executeAndWait( + query, + )) as Array; + + const countQuery: Query = { + ...query, + select: {}, + aggregates: { + count: { + type: 'fn', + fn: 'count', + args: [1], + }, + }, + groupBy: [], + cardinality: 'one', + }; + + const countResult = (await queryEngine.executeAndWait( + countQuery, + )) as any; + + expect(typeof countResult.count).toEqual('number'); + + expect(queryResult.length).toEqual(countResult.count); + }, + timeout, +); diff --git a/packages/docs/static/reference/assets/navigation.js b/packages/docs/static/reference/assets/navigation.js index ad00d094..97c58f37 100644 --- a/packages/docs/static/reference/assets/navigation.js +++ b/packages/docs/static/reference/assets/navigation.js @@ -1 +1 @@ -window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA52WUW/bIBCA/wvP2bpma7flrcsirdu0ZEm0aaqqiuJLjUqwA7iqNe2/T8Z2bAMG0lfz8R3gO46bv0jBs0IzdI/JI/DkTAqCJijHKkUztM+SgoE8awbvpCCvU7VnaIIeKU/QbDpBJKUsEcDR7OYo+1mAKBf8gXLoZIRhKQ1ZDxyKz6cf/k1cvpXIctlJKVcgdpiMe/UEY9UXl6Z8xTCPs1akT7d4zgVIuSm5Sg/sC+YJA9GpVZkbVucEI8Kbj+/PL6ahKGs4FCDVycGaeS+MKfOMS3hB0HpiKCrJGAOivuP+xnYFJ4pmfBinhw6tl+/6QgFYQeA3jQQYn2oFvO2HZNRdWIRRR1H1ZlKuRCZzIMot6Maji/MBOAiswLVZQ9ei3s0dChAUpHt9zWD04uYZK/b8M+ycxdi3HUlfMR6h6hYAoSi47w6nuJsTFeIEs1e4ISnscVBWYz7RFt8ziDnKFvTJPlGORbnMzTrve1omVNItV+VWZt2NLmNNhrxzLBLKMaOq9El7WNCo/5hXpok4z7bMrXvSdlVUnO8XZkWEUGMh4zUnrEi8tgYJmb5mlPvzpCZCHt1tfRoNRFnWIAtm9UXLVWMh4xp2/u1pIGTZQNWqfJqaCHl07fo0GghZfqcgvBYNRFnmDBf2c8By1Vjwprj+cbX+c7dcLdZX2+V602mfsKDVzoz7wuCH/rfGu8LVAfs2kjHPO4JKIxPckgbziA7DRHdrNPTq3NuJBeCxd4Ieiu7CzdNmnnH9wdU/OuEQ9na2mtS19g2s2raULRjKEjmy3C5HOvWQNQ90kCHNKlYie6KJ+3FoLbmFvXkzeg4usYV71PIUsQE7cqsTFxKaVfidHRcls/6Y3+lMMV0Dt/8Bd3lmf8kOAAA=" \ No newline at end of file +window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAE52WUW/bIBCA/wvP2bpma7flrcsirdu0ZEm0aaqqiuJLjUqwA7iqNe2/T8Z2bAMG0lfz8R3gO46bv0jBs0IzdI/JI/DkTAqCJijHKkUztM+SgoE8awbvpCCvU7VnaIIeKU/QbDpBJKUsEcDR7OYo+1mAKBf8gXLoZIRhKQ1ZDxyKz6cf/k1cvpXIctlJKVcgdpiMe/UEY9UXl6Z8xTCPs1akT7d4zgVIuSm5Sg/sC+YJA9GpVZkbVucEI8Kbj+/PL6ahKGs4FCDVycGaeS+MKfOMS3hB0HpiKCrJGAOivuP+xnYFJ4pmfBinhw6tl+/6QgFYQeA3jQQYn2oFvO2HZNRdWIRRR1H1ZlKuRCZzIMot6Maji/MBOAiswLVZQ9ei3s0dChAUpHt9zWD04uYZK/b8M+ycxdi3HUlfMR6h6hYAoSi47w6nuJsTFeIEs1e4ISnscVBWYz7RFt8ziDnKFvTJPlGORbnMzTrve1omVNItV+VWZt2NLmNNhrxzLBLKMaOq9El7WNCo/5hXpok4z7bMrXvSdlVUnO8XZkWEUGMh4zUnrEi8tgYJmb5mlPvzpCZCHt1tfRoNRFnWIAtm9UXLVWMh4xp2/u1pIGTZQNWqfJqaCHl07fo0GghZfqcgvBYNRFnmDBf2c8By1Vjwprj+cbX+c7dcLdZX2+V602mfsKDVzoz7wuCH/rfGu8LVAfs2kjHPO4JKIxPckgbziA7DRHdrNPTq3NuJBeCxd4Ieiu7CzdNmnnH9wdU/OuEQ9na2mtS19g2s2raULRjKEjmy3C5HOvWQNQ90kCHNKlYie6KJ+3FoLbmFvXkzeg4usYV71PIUsQE7cqsTFxKaVfidHRcls/6Y3+lMMV0Dt/8Bd3lmf8kOAAA=" \ No newline at end of file diff --git a/packages/docs/static/reference/assets/search.js b/packages/docs/static/reference/assets/search.js index 5576fcab..04323c09 100644 --- a/packages/docs/static/reference/assets/search.js +++ b/packages/docs/static/reference/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file +window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file diff --git a/packages/queries/src/ExperimentalQueryBuilder.ts b/packages/queries/src/ExperimentalQueryBuilder.ts new file mode 100644 index 00000000..2b1534a0 --- /dev/null +++ b/packages/queries/src/ExperimentalQueryBuilder.ts @@ -0,0 +1,147 @@ +// ignore type errors in this file +// @ts-nocheck +import { Exp } from './expression/Exp'; +import { DB } from './generated'; +import { BinaryOperator } from './types/BinaryOp'; +import { Table } from './types/Table'; + +type Primitive = string | number | boolean | null | undefined | Date; + +type GetFromScope = Key extends keyof Scope ? Scope[Key] : never; + +type ExpressionType = E extends string + ? GetFromScope + : E extends { $returnType?: infer T } + ? T + : E extends Primitive + ? E + : never; + +type RowType = { + [col in keyof DB[T]['columns']]: DB[T]['columns'][col] extends { + type: infer TType; + } + ? TType + : never; +}; + +type Query = { + from: string; + columns: string[]; + where: BooleanExpressions; + groupBy: string[]; + aggregates: Record>; + having: BooleanExpressions; + orderBy: string[]; + limit: number; + offset: number; +}; + +/** + * @see https://www.postgresql.org/docs/current/functions-comparison.html + */ +function eq(a: Exp, b: Exp): BooleanExpressions { + return { + type: 'op', + op: '=', + args: [a, b], + }; +} + +/** + * @see https://www.postgresql.org/docs/current/functions-logical.html + */ +function not(a: BooleanExpressions): BooleanExpressions { + return { + type: 'op', + op: 'not', + arg: a, + }; +} + +function add( + a: NumberExpressions, + b: NumberExpressions, +): NumberExpressions { + return { + type: 'op', + op: '+', + args: [a, b], + }; +} + +function count(a: Exp): NumberExpressions { + return { + type: 'fn', + fn: 'count', + args: [a], + }; +} + +function str(value: string): StringExpressions { + return { + type: 'const', + value, + }; +} + +function or( + ...args: BooleanExpressions[] +): BooleanExpressions { + return { + type: 'op', + op: '||', + args, + }; +} + +export class ExperimentalQueryBuilder { + private _filter: Exp | undefined; + + constructor(private table: string) {} + + filter(exp: Exp): ExperimentalQueryBuilder { + this._filter = exp; + return this; + } + + count() { + return this.aggregate({ count: count(1) }) + .select('count') + .take(1); + } + + select( + ...columns: TSelection[] + ): ExperimentalQueryBuilder> { + return this; + } + + groupBy( + ...columns: TSelection[] + ): ExperimentalQueryBuilder { + return this; + } + + aggregate>>( + aggregates: TAggregates, + ): ExperimentalQueryBuilder { + return this; + } + + take(count: number): Scope { + return this; + } +} + +function from_experimental>(table: TTable) { + return new ExperimentalQueryBuilder>(table); +} + +from_experimental('actor').count(); + +from_experimental('actor') + .filter(or(eq(str('asdf'), 'first_name'), eq(str('asdf'), 'last_name'))) + .groupBy('last_name') + .aggregate({ count: count(1) }) + .select('count', 'last_name'); diff --git a/packages/queries/src/expression/Exp.ts b/packages/queries/src/expression/Exp.ts new file mode 100644 index 00000000..56b9b627 --- /dev/null +++ b/packages/queries/src/expression/Exp.ts @@ -0,0 +1,38 @@ +import { BinaryOperator } from '../types/BinaryOp'; +import { FilterByValue } from '../types/FilterByValue'; + +export type BooleanExpressions = + | { type: 'const'; value: boolean; $returnType?: boolean } + | { + type: 'op'; + op: BinaryOperator; + args: Exp[]; + $returnType?: boolean; + } + | { type: 'op'; op: 'not'; arg: Exp; $returnType?: boolean } + | FilterByValue; + +export type NumberExpressions = + | number + | { type: 'const'; value: number; $returnType?: number } + | { type: 'op'; op: '+'; args: Exp[]; $returnType?: number } + | { type: 'fn'; fn: 'count'; args: Exp[]; $returnType?: number } + | FilterByValue; + +export type StringExpressions = + | FilterByValue + | { type: 'const'; value: string; $returnType?: string }; + +export type Exp = + // Case: boolean + ReturnType extends boolean + ? BooleanExpressions + : // Case: number + ReturnType extends number + ? NumberExpressions + : ReturnType extends string + ? StringExpressions + : // Case: no match + | BooleanExpressions + | NumberExpressions + | StringExpressions; diff --git a/packages/queries/src/expression/Expression.ts b/packages/queries/src/expression/Expression.ts deleted file mode 100644 index b998740a..00000000 --- a/packages/queries/src/expression/Expression.ts +++ /dev/null @@ -1,173 +0,0 @@ -export type Primitive = string | number | boolean | null | Date; - -export const unaryOperators = ['not', '-', 'exists', 'not exists'] as const; - -export type UnaryOperator = (typeof unaryOperators)[number]; - -export const binaryOperators = [ - '=', - '==', - '!=', - '<>', - '>', - '>=', - '<', - '<=', - 'in', - 'not in', - 'is', - 'is not', - 'like', - 'not like', - 'match', - 'ilike', - 'not ilike', - '@>', - '<@', - '&&', - '?', - '?&', - '!<', - '!>', - '<=>', - '!~', - '~', - '~*', - '!~*', - '@@', - '@@@', - '!!', - '<->', - 'regexp', - '+', - '-', - '*', - '/', - '%', - '^', - '&', - '|', - '#', - '<<', - '>>', - '&&', - '||', -] as const; - -export type BinaryOperator = (typeof binaryOperators)[number]; - -export const nAryOperators = [ - 'and', - 'or', - '=', - '==', - '!=', - '<>', - '>', - '>=', - '<', - '<=', - 'in', - 'not in', - 'is', - 'is not', - 'like', - '#>', - '#>>', - 'not like', - 'match', - 'ilike', - 'not ilike', - '@>', - '<@', - '&&', - '?', - '?&', - '!<', - '!>', - '<=>', - '!~', - '~', - '~*', - '!~*', - '@@', - '@@@', - '!!', - '<->', - 'regexp', - '+', - '-', - '*', - '/', - '%', - '^', - '&', - '|', - '#', - '<<', - '>>', - '&&', - '||', - '->', - '->>', - 'not', - '-', - 'exists', - 'not exists', - 'between', - 'between symmetric', -] as const; - -export type NAryOperator = (typeof nAryOperators)[number]; - -/** - * An expression that invokes a function - */ -export type ExpFunctionInvocation = [ - '>invoke', - functionName: string, - ...Exp[], -]; - -/** - * An expression that references a column in a table - */ -export type ExpColumnReference = Context; - -/** - * An expression that casts an expression to a different type - */ -export type ExpCast = ['>::', exp: Exp, type: string]; - -/** - * An expression that conditionally evaluates to one of two expressions - */ -export type ExpWhen = [ - '>when', - condition: Exp, - whenTrue: Exp, - whenFalse: Exp, -]; - -/** - * A literal is serialized as a value in the query - * e.g. `SELECT * FROM table WHERE column = 1` - */ -export type ExpLiteral = ['>literal', Primitive] | Exclude; - -/** - * A parameter is serialized as a placeholder in the query - * e.g. `SELECT * FROM table WHERE column = $1` - */ -export type ExpParam = ['>param', Primitive]; - -export type Exp = - | ExpLiteral - | ExpParam - | ExpFunctionInvocation - | ExpColumnReference - | ExpCast - | ExpWhen - | [UnaryOperator, Exp] - | [BinaryOperator, Exp, Exp] - | [NAryOperator, Exp, ...Exp[]]; diff --git a/packages/queries/src/expression/dsl.ts b/packages/queries/src/expression/dsl.ts deleted file mode 100644 index ea01d0fa..00000000 --- a/packages/queries/src/expression/dsl.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Exp, Primitive } from './Expression'; - -export function equals( - exp1: Exp, - exp2: Exp, -): Exp { - return ['=', exp1, exp2]; -} - -export function isPositive(exp: Exp): Exp { - return ['>', exp, 0]; -} - -export function isNegative(exp: Exp): Exp { - return ['<', exp, 0]; -} - -export function isZero(exp: Exp): Exp { - return ['=', exp, 0]; -} - -export function coalesce( - exp: Exp, - def: Exp, -): Exp { - return ['>invoke', 'coalesce', exp, def]; -} - -export function isNotNull(exp: Exp): Exp { - return ['is not', exp, null]; -} - -export function isNull(exp: Exp): Exp { - return ['is', exp, null]; -} - -export function sum(exp: Exp): Exp { - return ['>invoke', 'sum', exp]; -} - -export function param( - param: TParam, -): Exp { - return ['>param', param]; -} - -export function literal( - param: TParam, -): Exp { - return ['>literal', param]; -} - -export function when( - condition: Exp, - whenTrue: Exp, - whenFalse: Exp, -): Exp { - return ['>when', condition, whenTrue, whenFalse]; -} - -export const cast = { - asText: (exp: Exp): Exp => { - return ['>::', exp, 'text']; - }, - asNumeric: (exp: Exp): Exp => { - return ['>::', exp, 'numeric']; - }, - asInteger: (exp: Exp): Exp => { - return ['>::', exp, 'integer']; - }, - asUuid: (exp: Exp): Exp => { - return ['>::', exp, 'uuid']; - }, -}; - -export const json = { - get: (exp: Exp, key: string): Exp => { - return ['->', exp, literal(key)]; - }, - getAsText: (exp: Exp, key: string): Exp => { - return ['->>', exp, literal(key)]; - }, - getAsNumeric: (exp: Exp, key: string): Exp => { - const jsonText = json.getAsText(exp, key); - const withDefault = coalesce(jsonText, literal('0')); - - return cast.asNumeric(withDefault); - }, - agg: (exp: Exp): Exp => { - return ['>invoke', 'json_agg', exp]; - }, - buildObject: (...exp: Exp[]): Exp => { - return ['>invoke', 'json_build_object', ...exp]; - }, -}; - -export function distinct(exp: Exp): Exp { - return ['>invoke', 'distinct', exp]; -} - -export function count(exp: Exp): Exp { - return ['>invoke', 'count', exp]; -} - -export const jsonb = { - buildObject: (...exp: Exp[]): Exp => { - return ['>invoke', 'jsonb_build_object', ...exp]; - }, - agg: (exp: Exp): Exp => { - return ['>invoke', 'jsonb_agg', exp]; - }, -}; - -export function chain( - exp: Exp, - ...fns: Array<(exp: Exp) => Exp> -): Exp { - return fns.reduce((acc, fn): Exp => { - return fn(acc); - }, exp); -} diff --git a/packages/queries/src/expression/fns.ts b/packages/queries/src/expression/fns.ts new file mode 100644 index 00000000..c607b644 --- /dev/null +++ b/packages/queries/src/expression/fns.ts @@ -0,0 +1,9 @@ +import { Exp, NumberExpressions } from './Exp'; + +export function count(a: Exp): NumberExpressions { + return { + type: 'fn', + fn: 'count', + args: [a], + }; +} diff --git a/packages/queries/src/expression/ops.ts b/packages/queries/src/expression/ops.ts new file mode 100644 index 00000000..9be226f0 --- /dev/null +++ b/packages/queries/src/expression/ops.ts @@ -0,0 +1,34 @@ +import { BooleanExpressions, Exp, NumberExpressions } from './Exp'; + +export function add( + a: NumberExpressions, + b: NumberExpressions, +): NumberExpressions { + return { + type: 'op', + op: '+', + args: [a, b], + }; +} + +/** + * @see https://www.postgresql.org/docs/current/functions-comparison.html + */ +function eq(a: Exp, b: Exp): BooleanExpressions { + return { + type: 'op', + op: '=', + args: [a, b], + }; +} + +/** + * @see https://www.postgresql.org/docs/current/functions-logical.html + */ +function not(a: BooleanExpressions): BooleanExpressions { + return { + type: 'op', + op: 'not', + arg: a, + }; +} diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index 13c60ff0..f04b07b3 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -1,5 +1,5 @@ export { col } from './col'; -export * from './types/types'; +export * from './types/Query'; export * from './types/BinaryOp'; export * from './types/Cardinality'; export * from './types/Column'; diff --git a/packages/queries/src/query.ts b/packages/queries/src/query.ts index 70b7fca8..567d025b 100644 --- a/packages/queries/src/query.ts +++ b/packages/queries/src/query.ts @@ -6,6 +6,7 @@ import { Table } from './types/Table'; import { Schema } from './types/Schema'; import { getTableSelectableColumns } from './schema/getTableSelectableColumns'; import { getTablePrimaryKeyColumns } from './schema/getTablePrimaryKeyColumns'; +import { Exp } from './expression/Exp'; export class QueryBuilder< DB, @@ -18,6 +19,7 @@ export class QueryBuilder< TCardinality extends 'one' | 'maybe' | 'many', TLazy extends true | undefined, TGroupBy extends string[], + TAggregates extends Record | undefined, > { constructor( private _from: TTable, @@ -29,6 +31,7 @@ export class QueryBuilder< private _cardinality: TCardinality, private _lazy: TLazy, private _groupBy: TGroupBy, + private _aggregates: TAggregates, ) {} private build(): { @@ -41,6 +44,7 @@ export class QueryBuilder< cardinality: TCardinality; lazy: TLazy; groupBy: TGroupBy; + aggregates: TAggregates; } { return { from: this._from, @@ -52,9 +56,58 @@ export class QueryBuilder< cardinality: this._cardinality ?? 'many', lazy: this._lazy, groupBy: this._groupBy, + aggregates: this._aggregates, }; } + /** + * Returns a query that counts the number of rows that match the query. + * + * Example: + * + * ```ts + * const query = from('actor') + * .where({ actor_id: {in: [1,2,3]} }) + * .count() + * + * const { data } = useSynthql({query}) + * + * console.log(data.count) // 3 + * ``` + * + * This will return a query that counts the number of rows in the `actor` table where `actor_id = 1`. + */ + count() { + return new QueryBuilder< + DB, + TTable, + TWhere, + {}, + TInclude, + undefined, + undefined, + 'one', + TLazy, + TGroupBy, + { + count: { type: 'fn'; fn: 'count'; args: [1] }; + } + >( + this._from, + this._where, + {}, + this._include, + undefined, + undefined, + 'one', + this._lazy, + this._groupBy, + { + count: { type: 'fn', fn: 'count', args: [1] }, + }, + ).build(); + } + /** * Sets the limit of the query. */ @@ -69,7 +122,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -80,6 +134,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -97,7 +152,8 @@ export class QueryBuilder< TOffset, 'many', TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -108,6 +164,7 @@ export class QueryBuilder< 'many', this._lazy, this._groupBy, + this._aggregates, ).build(); } @@ -125,7 +182,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -136,6 +194,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -155,7 +214,8 @@ export class QueryBuilder< TOffset, 'one', TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -166,6 +226,7 @@ export class QueryBuilder< 'one', this._lazy, this._groupBy, + this._aggregates, ).build(); } @@ -183,7 +244,8 @@ export class QueryBuilder< TOffset, 'many', TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -194,6 +256,7 @@ export class QueryBuilder< 'many', this._lazy, this._groupBy, + this._aggregates, ).build(); } @@ -211,7 +274,8 @@ export class QueryBuilder< TOffset, 'maybe', TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -222,6 +286,7 @@ export class QueryBuilder< 'maybe', this._lazy, this._groupBy, + this._aggregates, ).build(); } @@ -236,7 +301,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -247,6 +313,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -283,7 +350,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -294,6 +362,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -308,7 +377,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -319,6 +389,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -333,7 +404,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -344,6 +416,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -362,7 +435,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, where, @@ -373,6 +447,7 @@ export class QueryBuilder< this._cardinality, this._lazy, this._groupBy, + this._aggregates, ); } @@ -387,7 +462,8 @@ export class QueryBuilder< TOffset, TCardinality, true, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -398,6 +474,7 @@ export class QueryBuilder< this._cardinality, true, this._groupBy, + this._aggregates, ); } @@ -412,7 +489,8 @@ export class QueryBuilder< TOffset, TCardinality, TLazy, - TGroupBy + TGroupBy, + TAggregates >( this._from, this._where, @@ -423,6 +501,7 @@ export class QueryBuilder< this._cardinality, this._lazy, id, + this._aggregates, ); } } @@ -446,7 +525,8 @@ export function query(schema: Schema) { number | undefined, 'many', undefined, - typeof primaryKeys + typeof primaryKeys, + undefined >( table, {}, @@ -457,6 +537,7 @@ export function query(schema: Schema) { 'many', undefined, primaryKeys, + undefined, ); }, }; diff --git a/packages/queries/src/types/FilterByValue.ts b/packages/queries/src/types/FilterByValue.ts new file mode 100644 index 00000000..85524d99 --- /dev/null +++ b/packages/queries/src/types/FilterByValue.ts @@ -0,0 +1,16 @@ +/** + * Returns the keys of TRecord that extend TValue + * + * Example: + * ```ts + * type Record = { + * name: string; + * age: number; + * } + * + * FilterByValue // 'name' + * FilterByValue // 'age' + */ +export type FilterByValue = { + [Key in keyof TRecord]: TRecord[Key] extends TValue ? Key : never; +}[keyof TRecord]; diff --git a/packages/queries/src/types/Include.ts b/packages/queries/src/types/Include.ts index 06d243f5..a459abe6 100644 --- a/packages/queries/src/types/Include.ts +++ b/packages/queries/src/types/Include.ts @@ -1,5 +1,5 @@ import { Table } from './Table'; -import { Query } from './types'; +import { Query } from './Query'; export type Include = { [k in string]: Query> extends Query diff --git a/packages/queries/src/types/types.ts b/packages/queries/src/types/Query.ts similarity index 82% rename from packages/queries/src/types/types.ts rename to packages/queries/src/types/Query.ts index 725d2adc..ccd4d7e4 100644 --- a/packages/queries/src/types/types.ts +++ b/packages/queries/src/types/Query.ts @@ -2,6 +2,7 @@ import { Table } from './Table'; import { Select } from './Select'; import { Where } from './Where'; import { Include } from './Include'; +import { Exp } from '../expression/Exp'; export type Query = Table> = { from: TTable; @@ -13,4 +14,7 @@ export type Query = Table> = { cardinality?: 'one' | 'maybe' | 'many'; lazy?: true; groupBy?: string[]; + aggregates?: { + [key: string]: Exp; + }; }; diff --git a/packages/queries/src/types/QueryResult.ts b/packages/queries/src/types/QueryResult.ts index 984a02d6..573c6a34 100644 --- a/packages/queries/src/types/QueryResult.ts +++ b/packages/queries/src/types/QueryResult.ts @@ -1,7 +1,8 @@ -import { Query } from './types'; +import { Query } from './Query'; import { ColumnValue } from './ColumnValue'; import { Column } from './Column'; import { Table } from './Table'; +import { Simplify } from './Simplify'; export type QueryResult = Simplify< TQuery extends Query @@ -9,15 +10,6 @@ export type QueryResult = Simplify< : never >; -type Simplify = - T extends Array - ? Simplify[] - : T extends Date - ? T - : T extends object - ? { [K in keyof T]: Simplify } - : T; - type QueryResultInner< DB, TTable extends Table, diff --git a/packages/queries/src/types/Simplify.ts b/packages/queries/src/types/Simplify.ts new file mode 100644 index 00000000..b08810ac --- /dev/null +++ b/packages/queries/src/types/Simplify.ts @@ -0,0 +1,10 @@ +export type Simplify = + T extends Array + ? Simplify[] + : T extends Date + ? T + : T extends object + ? { + [K in keyof T]: Simplify; + } + : T;