Skip to content

Commit

Permalink
fix(zql): Fix type of IN/LIKE operators (#59)
Browse files Browse the repository at this point in the history
The RHS of IN needs to be an array
The RHS of LIKE/ILIKE needs to be a string
  • Loading branch information
arv committed Apr 8, 2024
1 parent ff2546a commit d8818ce
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 51 deletions.
22 changes: 9 additions & 13 deletions src/zql/ast/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 134 additions & 19 deletions src/zql/query/entity-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as agg from './agg.js';
import {conditionToString} from './condition-to-string.js';
import {
EntityQuery,
FieldValue,
WhereCondition,
and,
astForTesting,
Expand Down Expand Up @@ -120,6 +121,116 @@ test('query types', () => {
);
});

test('FieldValue type', () => {
type E = {
id: string;
n: number;
s: string;
b: boolean;
optN?: number | undefined;
optS?: string | undefined;
optB?: boolean | undefined;
};
expectTypeOf<FieldValue<E, 'id', '='>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'n', '='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 's', '!='>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'b', '='>>().toEqualTypeOf<boolean>();
expectTypeOf<FieldValue<E, 'optN', '='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'optS', '!='>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optB', '='>>().toEqualTypeOf<boolean>();

// booleans not allowed with order operators
expectTypeOf<FieldValue<E, 'b', '<'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'b', '<='>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'b', '>'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'b', '>='>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'n', '<'>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'n', '<='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'n', '>'>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'n', '>='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 's', '<'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 's', '<='>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 's', '>'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 's', '>='>>().toEqualTypeOf<string>();

expectTypeOf<FieldValue<E, 'optB', '<'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optB', '<='>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optB', '>'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optB', '>='>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optN', '<'>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'optN', '<='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'optN', '>'>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'optN', '>='>>().toEqualTypeOf<number>();
expectTypeOf<FieldValue<E, 'optS', '<'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optS', '<='>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optS', '>'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optS', '>='>>().toEqualTypeOf<string>();

expectTypeOf<FieldValue<E, 'n', 'IN'>>().toEqualTypeOf<number[]>();
expectTypeOf<FieldValue<E, 'n', 'NOT IN'>>().toEqualTypeOf<number[]>();
expectTypeOf<FieldValue<E, 's', 'IN'>>().toEqualTypeOf<string[]>();
expectTypeOf<FieldValue<E, 's', 'NOT IN'>>().toEqualTypeOf<string[]>();
expectTypeOf<FieldValue<E, 'b', 'IN'>>().toEqualTypeOf<boolean[]>();
expectTypeOf<FieldValue<E, 'b', 'NOT IN'>>().toEqualTypeOf<boolean[]>();

expectTypeOf<FieldValue<E, 'optN', 'IN'>>().toEqualTypeOf<number[]>();
expectTypeOf<FieldValue<E, 'optN', 'NOT IN'>>().toEqualTypeOf<number[]>();
expectTypeOf<FieldValue<E, 'optS', 'IN'>>().toEqualTypeOf<string[]>();
expectTypeOf<FieldValue<E, 'optS', 'NOT IN'>>().toEqualTypeOf<string[]>();
expectTypeOf<FieldValue<E, 'optB', 'IN'>>().toEqualTypeOf<boolean[]>();
expectTypeOf<FieldValue<E, 'optB', 'NOT IN'>>().toEqualTypeOf<boolean[]>();

expectTypeOf<FieldValue<E, 'n', 'LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'n', 'NOT LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 's', 'LIKE'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 's', 'NOT LIKE'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'b', 'LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'b', 'NOT LIKE'>>().toEqualTypeOf<never>();

expectTypeOf<FieldValue<E, 'optN', 'LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optN', 'NOT LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optS', 'LIKE'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optS', 'NOT LIKE'>>().toEqualTypeOf<string>();
expectTypeOf<FieldValue<E, 'optB', 'LIKE'>>().toEqualTypeOf<never>();
expectTypeOf<FieldValue<E, 'optB', 'NOT LIKE'>>().toEqualTypeOf<never>();

const q = new EntityQuery<E>(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(),
Expand Down Expand Up @@ -699,57 +810,61 @@ describe('NOT', () => {
});

describe("De Morgan's Law", () => {
type S = E1;
type S = {
id: string;
n: number;
s: string;
};

const cases: {
condition: WhereCondition<S>;
expected: WhereCondition<S>;
}[] = [
{
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'),
},
];

Expand Down
62 changes: 43 additions & 19 deletions src/zql/query/entity-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import {
AST,
Aggregation,
Condition,
EqualityOps,
InOps,
LikeOps,
OrderOps,
Primitive,
SimpleOperator,
} from '../ast/ast.js';
Expand All @@ -12,10 +16,26 @@ import {EntitySchema} from '../schema/entity-schema.js';
import {AggArray, Aggregate, Count, isAggregate} from './agg.js';
import {Statement} from './statement.js';

type FieldValue<S extends EntitySchema, K extends Selectable<S>> = S[K] extends
| Primitive
| undefined
? S[K]
type NotUndefined<T> = Exclude<T, undefined>;

export type FieldValue<
S extends EntitySchema,
K extends Selectable<S>,
Op extends SimpleOperator,
> = S[K] extends Primitive | undefined
? Op extends InOps
? NotUndefined<S[K]>[]
: Op extends LikeOps
? S[K] extends string | undefined
? NotUndefined<S[K]>
: never
: Op extends OrderOps
? S[K] extends boolean | undefined
? never
: NotUndefined<S[K]>
: Op extends EqualityOps
? NotUndefined<S[K]>
: never
: never;

type AggregateValue<S extends EntitySchema, K extends Aggregable<S>> =
Expand Down Expand Up @@ -79,14 +99,18 @@ export type WhereCondition<S extends EntitySchema> =
op: 'AND' | 'OR';
conditions: WhereCondition<S>[];
}
| SimpleCondition<S>;
| SimpleCondition<S, Selectable<S>, SimpleOperator>;

type SimpleCondition<S extends EntitySchema> = {
type SimpleCondition<
S extends EntitySchema,
K extends Selectable<S>,
Op extends SimpleOperator,
> = {
op: SimpleOperator;
field: Selectable<S>;
field: K;
value: {
type: 'literal';
value: FieldValue<S, Selectable<S>>;
value: FieldValue<S, K, Op>;
};
};

Expand Down Expand Up @@ -138,15 +162,15 @@ export class EntityQuery<S extends EntitySchema, Return = []> {
}

where(expr: WhereCondition<S>): EntityQuery<S, Return>;
where<K extends Selectable<S>>(
where<K extends Selectable<S>, Op extends SimpleOperator>(
field: K,
op: SimpleOperator,
value: FieldValue<S, K>,
op: Op,
value: FieldValue<S, K, Op>,
): EntityQuery<S, Return>;
where<K extends Selectable<S>>(
where<K extends Selectable<S>, Op extends SimpleOperator>(
exprOrField: K | WhereCondition<S>,
op?: SimpleOperator,
value?: FieldValue<S, K>,
op?: Op,
value?: FieldValue<S, K, Op>,
): EntityQuery<S, Return> {
let expr: WhereCondition<S>;
if (typeof exprOrField === 'string') {
Expand Down Expand Up @@ -249,11 +273,11 @@ function flatten<S extends EntitySchema>(
return {op, conditions: flattened};
}

export function expression<S extends EntitySchema, K extends Selectable<S>>(
field: K,
op: SimpleOperator,
value: FieldValue<S, K>,
): WhereCondition<S> {
export function expression<
S extends EntitySchema,
K extends Selectable<S>,
Op extends SimpleOperator,
>(field: K, op: Op, value: FieldValue<S, K, Op>): WhereCondition<S> {
return {
op,
field,
Expand Down

0 comments on commit d8818ce

Please sign in to comment.