diff --git a/src/zql/ast/ast.ts b/src/zql/ast/ast.ts index ba5bcb3..ff4f738 100644 --- a/src/zql/ast/ast.ts +++ b/src/zql/ast/ast.ts @@ -50,19 +50,15 @@ export type Conjunction = { op: 'AND' | 'OR'; conditions: Condition[]; }; -export type SimpleOperator = - | '=' - | '!=' - | '<' - | '>' - | '>=' - | '<=' - | 'IN' - | 'NOT IN' - | 'LIKE' - | 'NOT LIKE' - | 'ILIKE' - | 'NOT ILIKE'; +export type SimpleOperator = EqualityOps | OrderOps | InOps | LikeOps; + +export type EqualityOps = '=' | '!='; + +export type OrderOps = '<' | '>' | '<=' | '>='; + +export type InOps = 'IN' | 'NOT IN'; + +export type LikeOps = 'LIKE' | 'NOT LIKE' | 'ILIKE' | 'NOT ILIKE'; export type SimpleCondition = // | ConditionList diff --git a/src/zql/query/entity-query.test.ts b/src/zql/query/entity-query.test.ts index c3958c9..7bd3ee3 100644 --- a/src/zql/query/entity-query.test.ts +++ b/src/zql/query/entity-query.test.ts @@ -6,6 +6,7 @@ import * as agg from './agg.js'; import {conditionToString} from './condition-to-string.js'; import { EntityQuery, + FieldValue, WhereCondition, and, astForTesting, @@ -120,6 +121,118 @@ test('query types', () => { ); }); +test('FieldValue type', () => { + type E = { + fields: { + id: string; + n: number; + s: string; + b: boolean; + optN?: number | undefined; + optS?: string | undefined; + optB?: boolean | undefined; + }; + }; + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf'>>().toEqualTypeOf(); + expectTypeOf='>>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + + const q = new EntityQuery(context, 'e'); + q.where('n', '<', 1); + q.where('s', '>', 'a'); + q.where('b', '=', true); + q.where('id', '=', 'a'); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', '<', false); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', '<=', false); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', '>', false); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', '>=', false); + + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345) + q.where('n', 'LIKE', 'abc'); + // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'never'.ts(2345) + q.where('n', 'ILIKE', 123); + q.where('s', 'LIKE', 'abc'); + // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'string'.ts(2345) + q.where('s', 'ILIKE', 123); + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', 'LIKE', 'abc'); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'never'.ts(2345) + q.where('b', 'ILIKE', true); + + q.where('n', 'IN', [1, 2, 3]); + // @ts-expect-error Argument of type 'number' is not assignable to parameter of type 'number[]'.ts(2345) + q.where('n', 'IN', 1); + q.where('s', 'IN', ['a', 'b', 'c']); + // @ts-expect-error Argument of type 'string' is not assignable to parameter of type 'string[]'.ts(2345) + q.where('s', 'IN', 'a'); + q.where('b', 'IN', [true, false]); + // @ts-expect-error Argument of type 'boolean' is not assignable to parameter of type 'boolean[]'.ts(2345) + q.where('b', 'IN', true); +}); + const e1 = z.object({ id: z.string(), a: z.number(), @@ -703,57 +816,63 @@ describe('NOT', () => { }); describe("De Morgan's Law", () => { - type S = {fields: E1}; + type S = { + fields: { + id: string; + n: number; + s: string; + }; + }; const cases: { condition: WhereCondition; expected: WhereCondition; }[] = [ { - condition: expression('a', '=', 1), - expected: expression('a', '!=', 1), + condition: expression('n', '=', 1), + expected: expression('n', '!=', 1), }, { - condition: and(expression('a', '!=', 1), expression('a', '<', 2)), - expected: or(expression('a', '=', 1), expression('a', '>=', 2)), + condition: and(expression('n', '!=', 1), expression('n', '<', 2)), + expected: or(expression('n', '=', 1), expression('n', '>=', 2)), }, { - condition: or(expression('a', '<=', 1), expression('a', '>', 2)), - expected: and(expression('a', '>', 1), expression('a', '<=', 2)), + condition: or(expression('n', '<=', 1), expression('n', '>', 2)), + expected: and(expression('n', '>', 1), expression('n', '<=', 2)), }, { condition: or( - and(expression('a', '>=', 1), expression('a', 'IN', 1)), - expression('a', 'NOT IN', 2), + and(expression('n', '>=', 1), expression('n', 'IN', [1, 2])), + expression('n', 'NOT IN', [3, 4]), ), expected: and( - or(expression('a', '<', 1), expression('a', 'NOT IN', 1)), - expression('a', 'IN', 2), + or(expression('n', '<', 1), expression('n', 'NOT IN', [1, 2])), + expression('n', 'IN', [3, 4]), ), }, { condition: and( - or(expression('a', 'NOT IN', 1), expression('a', 'LIKE', 1)), - expression('a', 'NOT LIKE', 2), + or(expression('n', 'NOT IN', [5, 6]), expression('s', 'LIKE', 'Hi')), + expression('s', 'NOT LIKE', 'Hi'), ), expected: or( - and(expression('a', 'IN', 1), expression('a', 'NOT LIKE', 1)), - expression('a', 'LIKE', 2), + and(expression('n', 'IN', [5, 6]), expression('s', 'NOT LIKE', 'Hi')), + expression('s', 'LIKE', 'Hi'), ), }, { - condition: not(expression('a', 'ILIKE', 1)), - expected: expression('a', 'ILIKE', 1), + condition: not(expression('s', 'ILIKE', 'hi')), + expected: expression('s', 'ILIKE', 'hi'), }, { - condition: not(expression('a', 'NOT ILIKE', 1)), - expected: expression('a', 'NOT ILIKE', 1), + condition: not(expression('s', 'NOT ILIKE', 'bye')), + expected: expression('s', 'NOT ILIKE', 'bye'), }, ]; diff --git a/src/zql/query/entity-query.ts b/src/zql/query/entity-query.ts index 0b9b21a..1551f5b 100644 --- a/src/zql/query/entity-query.ts +++ b/src/zql/query/entity-query.ts @@ -2,6 +2,10 @@ import { AST, Aggregation, Condition, + EqualityOps, + InOps, + LikeOps, + OrderOps, Primitive, SimpleOperator, } from '../ast/ast.js'; @@ -12,10 +16,27 @@ import {EntitySchema} from '../schema/entity-schema.js'; import {AggArray, Aggregate, Count, isAggregate} from './agg.js'; import {Statement} from './statement.js'; -type FieldValue< +type NotUndefined = Exclude; + +export type FieldValue< S extends EntitySchema, K extends Selectable, -> = S['fields'][K] extends Primitive | undefined ? S['fields'][K] : never; + Op extends SimpleOperator, +> = S['fields'][K] extends Primitive | undefined + ? Op extends InOps + ? NotUndefined[] + : Op extends LikeOps + ? S['fields'][K] extends string | undefined + ? NotUndefined + : never + : Op extends OrderOps + ? S['fields'][K] extends boolean | undefined + ? never + : NotUndefined + : Op extends EqualityOps + ? NotUndefined + : never + : never; type AggregateValue> = K extends Count @@ -86,14 +107,18 @@ export type WhereCondition = op: 'AND' | 'OR'; conditions: WhereCondition[]; } - | SimpleCondition; + | SimpleCondition, SimpleOperator>; -type SimpleCondition = { +type SimpleCondition< + S extends EntitySchema, + K extends Selectable, + Op extends SimpleOperator, +> = { op: SimpleOperator; - field: Selectable; + field: K; value: { type: 'literal'; - value: FieldValue>; + value: FieldValue; }; }; @@ -145,15 +170,15 @@ export class EntityQuery { } where(expr: WhereCondition): EntityQuery; - where>( + where, Op extends SimpleOperator>( field: K, - op: SimpleOperator, - value: FieldValue, + op: Op, + value: FieldValue, ): EntityQuery; - where>( + where, Op extends SimpleOperator>( exprOrField: K | WhereCondition, - op?: SimpleOperator, - value?: FieldValue, + op?: Op, + value?: FieldValue, ): EntityQuery { let expr: WhereCondition; if (typeof exprOrField === 'string') { @@ -256,11 +281,11 @@ function flatten( return {op, conditions: flattened}; } -export function expression>( - field: K, - op: SimpleOperator, - value: FieldValue, -): WhereCondition { +export function expression< + S extends EntitySchema, + K extends Selectable, + Op extends SimpleOperator, +>(field: K, op: Op, value: FieldValue): WhereCondition { return { op, field,