Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/prisma-next-demo/src/prisma/contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Interval } from '@prisma-next/adapter-postgres/codec-types';
import type { CodecTypes as PgVectorTypes } from '@prisma-next/extension-pgvector/codec-types';
import type { Vector } from '@prisma-next/extension-pgvector/codec-types';
import type { OperationTypes as PgVectorOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types';
import type { QueryOperationTypes as PgVectorQueryOperationTypes } from '@prisma-next/extension-pgvector/operation-types';

import type {
Expand All @@ -39,7 +40,7 @@ export type ProfileHash =
export type CodecTypes = PgTypes & PgVectorTypes;
export type OperationTypes = PgVectorOperationTypes;
export type LaneCodecTypes = CodecTypes;
export type QueryOperationTypes = PgVectorQueryOperationTypes;
export type QueryOperationTypes = PgAdapterQueryOps & PgVectorQueryOperationTypes;
type DefaultLiteralValue<CodecId extends string, _Encoded> = CodecId extends keyof CodecTypes
? CodecTypes[CodecId]['output']
: _Encoded;
Expand Down
8 changes: 7 additions & 1 deletion packages/1-framework/1-core/operations/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
export interface ParamSpec {
readonly codecId?: string;
readonly traits?: readonly string[];
readonly nullable: boolean;
}

export interface ReturnSpec {
readonly codecId: string;
readonly nullable: boolean;
}

export interface OperationEntry {
readonly args: readonly ParamSpec[];
readonly returns: ParamSpec;
readonly returns: ReturnSpec;
}

export type OperationDescriptor<T extends OperationEntry = OperationEntry> = T & {
Expand Down
1 change: 1 addition & 0 deletions packages/2-sql/1-core/contract/src/exports/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type {
Index,
OperationTypesOf,
PrimaryKey,
QueryOperationArgSpec,
QueryOperationTypeEntry,
QueryOperationTypesBase,
QueryOperationTypesOf,
Expand Down
9 changes: 8 additions & 1 deletion packages/2-sql/1-core/contract/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ColumnDefault, StorageBase } from '@prisma-next/contract/types';
import type { CodecTrait } from '@prisma-next/framework-components/codec';

/**
* A column definition in storage.
Expand Down Expand Up @@ -164,8 +165,14 @@ export type OperationTypesOf<T> = [T] extends [never]
: Record<string, never>
: Record<string, never>;

export type QueryOperationArgSpec = {
readonly codecId?: string;
readonly traits?: CodecTrait;
readonly nullable: boolean;
};

export type QueryOperationTypeEntry = {
readonly args: readonly { readonly codecId: string; readonly nullable: boolean }[];
readonly args: readonly QueryOperationArgSpec[];
readonly returns: { readonly codecId: string; readonly nullable: boolean };
};

Expand Down
16 changes: 1 addition & 15 deletions packages/2-sql/4-lanes/relational-core/src/ast/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,7 @@ import type { SqlLoweringSpec } from '@prisma-next/sql-operations';

export type Direction = 'asc' | 'desc';

export type BinaryOp =
| 'eq'
| 'neq'
| 'gt'
| 'lt'
| 'gte'
| 'lte'
| 'like'
| 'ilike'
| 'in'
| 'notIn';
export type BinaryOp = 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'like' | 'in' | 'notIn';

export type AggregateCountFn = 'count';
export type AggregateOpFn = 'sum' | 'avg' | 'min' | 'max';
Expand Down Expand Up @@ -801,10 +791,6 @@ export class BinaryExpr extends Expression {
return new BinaryExpr('like', left, right);
}

static ilike(left: AnyExpression, right: AnyExpression): BinaryExpr {
return new BinaryExpr('ilike', left, right);
}

static in(left: AnyExpression, right: AnyExpression): BinaryExpr {
return new BinaryExpr('in', left, right);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe('ast/predicate', () => {
'gte',
'lte',
'like',
'ilike',
'in',
'notIn',
] as const)('stores the %s operator', (op) => {
Expand Down
30 changes: 27 additions & 3 deletions packages/2-sql/4-lanes/sql-builder/src/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { QueryOperationTypesBase } from '@prisma-next/sql-contract/types';
import type {
Expand,
ExpressionType,
FieldSpec,
QueryContext,
Scope,
ScopeField,
ScopeTable,
Subquery,
} from './scope';

export type Expression<T extends ScopeField> = {
export type Expression<T extends FieldSpec> = {
[ExpressionType]: T;
buildAst(): import('@prisma-next/sql-relational-core/ast').AnyExpression;
};
Expand Down Expand Up @@ -66,10 +67,33 @@ export type OrderByScope<
namespaces: AvailableScope['namespaces'];
};

type CodecIdsWithTrait<
CT extends Record<string, { readonly input: unknown }>,
Trait extends string,
> = {
[K in keyof CT & string]: CT[K] extends { readonly traits: infer T }
? Trait extends T
? K
: never
: never;
}[keyof CT & string];

type ResolveExtArg<Arg, CT extends Record<string, { readonly input: unknown }>> = Arg extends {
readonly codecId: infer CId extends string;
readonly nullable: infer N extends boolean;
}
? ExpressionOrValue<{ codecId: CId; nullable: N }, CT>
: Arg extends {
readonly traits: infer T extends string;
readonly nullable: infer N extends boolean;
}
? ExpressionOrValue<{ codecId: CodecIdsWithTrait<CT, T>; nullable: N }, CT>
: never;

type ExtensionFunctionArgs<
Args extends readonly ScopeField[],
Args extends readonly unknown[],
CT extends Record<string, { readonly input: unknown }>,
> = { [I in keyof Args]: ExpressionOrValue<Args[I], CT> };
> = { [I in keyof Args]: ResolveExtArg<Args[I], CT> };

type DeriveExtFunctions<
OT extends QueryOperationTypesBase,
Expand Down
2 changes: 2 additions & 0 deletions packages/2-sql/4-lanes/sql-builder/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type Expand<T> = { [K in keyof T]: T[K] } & unknown;
export type EmptyRow = Record<never, ScopeField>;

export type ScopeField = { codecId: string; nullable: boolean };
export type TraitField = { traits: string; nullable: boolean };
export type FieldSpec = ScopeField | TraitField;
export type ScopeTable = Record<string, ScopeField>;

export type Scope = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Interval } from '@prisma-next/adapter-postgres/codec-types';
import type { CodecTypes as PgVectorTypes } from '@prisma-next/extension-pgvector/codec-types';
import type { Vector } from '@prisma-next/extension-pgvector/codec-types';
import type { OperationTypes as PgVectorOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types';
import type { QueryOperationTypes as PgVectorQueryOperationTypes } from '@prisma-next/extension-pgvector/operation-types';

import type {
Expand All @@ -39,7 +40,7 @@ export type ProfileHash =
export type CodecTypes = PgTypes & PgVectorTypes;
export type OperationTypes = PgVectorOperationTypes;
export type LaneCodecTypes = CodecTypes;
export type QueryOperationTypes = PgVectorQueryOperationTypes;
export type QueryOperationTypes = PgAdapterQueryOps & PgVectorQueryOperationTypes;
type DefaultLiteralValue<CodecId extends string, _Encoded> = CodecId extends keyof CodecTypes
? CodecTypes[CodecId]['output']
: _Encoded;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import { describe, expect, it } from 'vitest';
import { setupIntegrationTest } from './setup';

describe('integration: ilike (adapter operation)', () => {
const { db, runtime } = setupIntegrationTest();

it('ilike filters case-insensitively in WHERE', async () => {
const rows = await runtime().execute(
db()
.users.select('id', 'name')
.where((f, fns) => fns.ilike(f.name, '%alice%'))
.build(),
);
expect(rows).toHaveLength(1);
expect(rows[0]!.name).toBe('Alice');
});

it('ilike returns no rows when pattern does not match', async () => {
const rows = await runtime().execute(
db()
.users.select('id')
.where((f, fns) => fns.ilike(f.name, '%zzz%'))
.build(),
);
expect(rows).toHaveLength(0);
});
});

describe('integration: extension functions', () => {
const { db, runtime } = setupIntegrationTest();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
import { expectTypeOf, test } from 'vitest';
import type { BooleanCodecType, Expression } from '../../src/expression';
import { db } from './preamble';

test('extension function in select expression', () => {
Expand Down Expand Up @@ -28,3 +29,20 @@ test('extension function composed with builtins in where', () => {

expectTypeOf(filtered).toEqualTypeOf<SqlQueryPlan<{ id: number; title: string }>>();
});

test('ilike filters text fields in where', () => {
const filtered = db.users
.select('id', 'name')
.where((f, fns) => fns.ilike(f.name, '%alice%'))
.build();

expectTypeOf(filtered).toEqualTypeOf<SqlQueryPlan<{ id: number; name: string }>>();
});

test('ilike returns boolean expression', () => {
db.users.select('id').where((f, fns) => {
const result = fns.ilike(f.name, '%test%');
expectTypeOf(result).toExtend<Expression<BooleanCodecType>>();
return result;
});
});
39 changes: 29 additions & 10 deletions packages/3-extensions/sql-orm-client/src/model-accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,28 @@ export function createModelAccessor<
>;

const opsByCodecId = new Map<string, [string, SqlOperationEntry][]>();

function registerOp(codecId: string, op: [string, SqlOperationEntry]) {
let existing = opsByCodecId.get(codecId);
if (!existing) {
existing = [];
opsByCodecId.set(codecId, existing);
}
existing.push(op);
}

for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
const selfCodecId = entry.args[0]?.codecId;
if (selfCodecId) {
let existing = opsByCodecId.get(selfCodecId);
if (!existing) {
existing = [];
opsByCodecId.set(selfCodecId, existing);
const self = entry.args[0];
const op: [string, SqlOperationEntry] = [name, entry];
if (self?.codecId) {
registerOp(self.codecId, op);
} else if (self?.traits) {
for (const codec of context.codecs.values()) {
const codecTraits: readonly string[] = codec.traits ?? [];
if (self.traits.every((t) => codecTraits.includes(t))) {
registerOp(codec.id, op);
}
}
existing.push([name, entry]);
}
}

Expand Down Expand Up @@ -137,11 +150,14 @@ function createExtensionMethodFactory(
methodName: string,
entry: SqlOperationEntry,
context: ExecutionContext,
): (...args: unknown[]) => Partial<ComparisonMethodFns<unknown>> {
): (...args: unknown[]) => unknown {
const returnTraits = context.codecs.traitsOf(entry.returns.codecId);
const isPredicate = returnTraits.includes('boolean');

return (...args: unknown[]) => {
const userArgSpecs = entry.args.slice(1);
const astArgs = userArgSpecs.map((argSpec, i) => {
return ParamRef.of(args[i], { codecId: argSpec.codecId });
return ParamRef.of(args[i], argSpec.codecId ? { codecId: argSpec.codecId } : undefined);
});

const opExpr = new OperationExpr({
Expand All @@ -152,7 +168,10 @@ function createExtensionMethodFactory(
lowering: entry.lowering,
});

const returnTraits = context.codecs.traitsOf(entry.returns.codecId);
if (isPredicate) {
return opExpr;
}

const methods: Record<string, unknown> = {};

for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
Expand Down
55 changes: 41 additions & 14 deletions packages/3-extensions/sql-orm-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ export type ComparisonMethodFns<T> = {
gte(value: T): AnyExpression;
lte(value: T): AnyExpression;
like(pattern: string): AnyExpression;
ilike(pattern: string): AnyExpression;
in(values: readonly T[]): AnyExpression;
notIn(values: readonly T[]): AnyExpression;
isNull(): AnyExpression;
Expand Down Expand Up @@ -224,28 +223,60 @@ type MapArgsToJsTypes<
? [CodecArgJsType<Head, TCodecTypes>, ...MapArgsToJsTypes<Tail, TCodecTypes>]
: [];

type IsBooleanReturn<Returns, TCodecTypes extends Record<string, unknown>> = Returns extends {
readonly codecId: infer Id extends string;
}
? Id extends keyof TCodecTypes
? TCodecTypes[Id] extends { readonly traits: infer T }
? 'boolean' extends T
? true
: false
: false
: false
: false;

type QueryOperationMethod<Op, TCodecTypes extends Record<string, unknown>> = Op extends {
readonly args: readonly [unknown, ...infer UserArgs];
readonly returns: infer Returns;
}
? (
...args: MapArgsToJsTypes<UserArgs, TCodecTypes>
) => ComparisonMethods<
QueryOperationReturnJsType<Returns, TCodecTypes>,
QueryOperationReturnTraits<Returns, TCodecTypes>
>
? IsBooleanReturn<Returns, TCodecTypes> extends true
? (...args: MapArgsToJsTypes<UserArgs, TCodecTypes>) => AnyExpression
: (
...args: MapArgsToJsTypes<UserArgs, TCodecTypes>
) => ComparisonMethods<
QueryOperationReturnJsType<Returns, TCodecTypes>,
QueryOperationReturnTraits<Returns, TCodecTypes>
>
: never;

type OpMatchesField<Op, CodecId extends string, CT extends Record<string, unknown>> = Op extends {
readonly args: readonly [infer Self, ...(readonly unknown[])];
}
? Self extends { readonly codecId: CodecId }
? true
: Self extends { readonly traits: infer RequiredTraits extends string }
? CodecId extends keyof CT
? CT[CodecId] extends { readonly traits: infer FieldTraits }
? RequiredTraits extends FieldTraits
? true
: false
: false
: false
: false
: false;

type FieldOperations<
TContract extends Contract<SqlStorage>,
ModelName extends string,
FieldName extends string,
> = FieldCodecId<TContract, ModelName, FieldName> extends infer CodecId extends string
? ExtractQueryOperationTypes<TContract> extends infer AllOps
? {
[OpName in keyof AllOps & string as AllOps[OpName] extends {
readonly args: readonly [{ readonly codecId: CodecId }, ...(readonly unknown[])];
}
[OpName in keyof AllOps & string as OpMatchesField<
AllOps[OpName],
CodecId,
ExtractCodecTypes<TContract>
> extends true
? OpName
: never]: QueryOperationMethod<AllOps[OpName], ExtractCodecTypes<TContract>>;
}
Expand Down Expand Up @@ -330,10 +361,6 @@ export const COMPARISON_METHODS_META = {
traits: ['textual'],
create: scalarComparisonMethod('like'),
},
ilike: {
traits: ['textual'],
create: scalarComparisonMethod('ilike'),
},
asc: {
traits: ['order'],
create: (left) => () => OrderByItem.asc(left),
Expand Down
Loading
Loading