Skip to content

Commit 9d94a40

Browse files
committed
feat: make ilike postgres-specific via trait-targeted extension operations
Move ilike from a core comparison method to a postgres adapter-provided extension operation. Operation args now support targeting by trait (e.g., `traits: ['textual']`) in addition to codecId, allowing adapter operations to match any codec with the required traits. - Extend ParamSpec and QueryOperationArgSpec with optional `traits` field - Add FieldSpec/TraitField to sql-builder scope for trait-targeted expressions - Update FieldOperations type matching to support trait-based operations - Boolean-trait return detection makes predicate ops return AnyExpression - Register ilike in postgres adapter's queryOperations() - Remove ilike from BinaryOp union, BinaryExpr, ComparisonMethodFns - Remove ilike runtime guard from SQLite adapter - Fix tsdown exports stripping ./schema-sql in sql-contract-ts
1 parent f2f0208 commit 9d94a40

31 files changed

Lines changed: 238 additions & 85 deletions

File tree

examples/prisma-next-demo/src/prisma/contract.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { Interval } from '@prisma-next/adapter-postgres/codec-types';
1616
import type { CodecTypes as PgVectorTypes } from '@prisma-next/extension-pgvector/codec-types';
1717
import type { Vector } from '@prisma-next/extension-pgvector/codec-types';
1818
import type { OperationTypes as PgVectorOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
19+
import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types';
1920
import type { QueryOperationTypes as PgVectorQueryOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
2021

2122
import type {
@@ -39,7 +40,7 @@ export type ProfileHash =
3940
export type CodecTypes = PgTypes & PgVectorTypes;
4041
export type OperationTypes = PgVectorOperationTypes;
4142
export type LaneCodecTypes = CodecTypes;
42-
export type QueryOperationTypes = PgVectorQueryOperationTypes;
43+
export type QueryOperationTypes = PgAdapterQueryOps & PgVectorQueryOperationTypes;
4344
type DefaultLiteralValue<CodecId extends string, _Encoded> = CodecId extends keyof CodecTypes
4445
? CodecTypes[CodecId]['output']
4546
: _Encoded;

packages/1-framework/1-core/operations/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
export interface ParamSpec {
2+
readonly codecId?: string;
3+
readonly traits?: readonly string[];
4+
readonly nullable: boolean;
5+
}
6+
7+
export interface ReturnSpec {
28
readonly codecId: string;
39
readonly nullable: boolean;
410
}
511

612
export interface OperationEntry {
713
readonly args: readonly ParamSpec[];
8-
readonly returns: ParamSpec;
14+
readonly returns: ReturnSpec;
915
}
1016

1117
export type OperationDescriptor<T extends OperationEntry = OperationEntry> = T & {

packages/2-sql/1-core/contract/src/exports/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type {
1212
Index,
1313
OperationTypesOf,
1414
PrimaryKey,
15+
QueryOperationArgSpec,
1516
QueryOperationTypeEntry,
1617
QueryOperationTypesBase,
1718
QueryOperationTypesOf,

packages/2-sql/1-core/contract/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ColumnDefault, StorageBase } from '@prisma-next/contract/types';
2+
import type { CodecTrait } from '@prisma-next/framework-components/codec';
23

34
/**
45
* A column definition in storage.
@@ -164,8 +165,14 @@ export type OperationTypesOf<T> = [T] extends [never]
164165
: Record<string, never>
165166
: Record<string, never>;
166167

168+
export type QueryOperationArgSpec = {
169+
readonly codecId?: string;
170+
readonly traits?: CodecTrait;
171+
readonly nullable: boolean;
172+
};
173+
167174
export type QueryOperationTypeEntry = {
168-
readonly args: readonly { readonly codecId: string; readonly nullable: boolean }[];
175+
readonly args: readonly QueryOperationArgSpec[];
169176
readonly returns: { readonly codecId: string; readonly nullable: boolean };
170177
};
171178

packages/2-sql/4-lanes/relational-core/src/ast/types.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,7 @@ import type { SqlLoweringSpec } from '@prisma-next/sql-operations';
44

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

7-
export type BinaryOp =
8-
| 'eq'
9-
| 'neq'
10-
| 'gt'
11-
| 'lt'
12-
| 'gte'
13-
| 'lte'
14-
| 'like'
15-
| 'ilike'
16-
| 'in'
17-
| 'notIn';
7+
export type BinaryOp = 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'like' | 'in' | 'notIn';
188

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

804-
static ilike(left: AnyExpression, right: AnyExpression): BinaryExpr {
805-
return new BinaryExpr('ilike', left, right);
806-
}
807-
808794
static in(left: AnyExpression, right: AnyExpression): BinaryExpr {
809795
return new BinaryExpr('in', left, right);
810796
}

packages/2-sql/4-lanes/relational-core/test/ast/predicate.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ describe('ast/predicate', () => {
2929
'gte',
3030
'lte',
3131
'like',
32-
'ilike',
3332
'in',
3433
'notIn',
3534
] as const)('stores the %s operator', (op) => {

packages/2-sql/4-lanes/sql-builder/src/expression.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import type { QueryOperationTypesBase } from '@prisma-next/sql-contract/types';
22
import type {
33
Expand,
44
ExpressionType,
5+
FieldSpec,
56
QueryContext,
67
Scope,
78
ScopeField,
89
ScopeTable,
910
Subquery,
1011
} from './scope';
1112

12-
export type Expression<T extends ScopeField> = {
13+
export type Expression<T extends FieldSpec> = {
1314
[ExpressionType]: T;
1415
buildAst(): import('@prisma-next/sql-relational-core/ast').AnyExpression;
1516
};
@@ -66,10 +67,33 @@ export type OrderByScope<
6667
namespaces: AvailableScope['namespaces'];
6768
};
6869

70+
type CodecIdsWithTrait<
71+
CT extends Record<string, { readonly input: unknown }>,
72+
Trait extends string,
73+
> = {
74+
[K in keyof CT & string]: CT[K] extends { readonly traits: infer T }
75+
? Trait extends T
76+
? K
77+
: never
78+
: never;
79+
}[keyof CT & string];
80+
81+
type ResolveExtArg<Arg, CT extends Record<string, { readonly input: unknown }>> = Arg extends {
82+
readonly codecId: infer CId extends string;
83+
readonly nullable: infer N extends boolean;
84+
}
85+
? ExpressionOrValue<{ codecId: CId; nullable: N }, CT>
86+
: Arg extends {
87+
readonly traits: infer T extends string;
88+
readonly nullable: infer N extends boolean;
89+
}
90+
? Expression<{ codecId: CodecIdsWithTrait<CT, T>; nullable: N }>
91+
: never;
92+
6993
type ExtensionFunctionArgs<
70-
Args extends readonly ScopeField[],
94+
Args extends readonly unknown[],
7195
CT extends Record<string, { readonly input: unknown }>,
72-
> = { [I in keyof Args]: ExpressionOrValue<Args[I], CT> };
96+
> = { [I in keyof Args]: ResolveExtArg<Args[I], CT> };
7397

7498
type DeriveExtFunctions<
7599
OT extends QueryOperationTypesBase,

packages/2-sql/4-lanes/sql-builder/src/scope.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type Expand<T> = { [K in keyof T]: T[K] } & unknown;
1414
export type EmptyRow = Record<never, ScopeField>;
1515

1616
export type ScopeField = { codecId: string; nullable: boolean };
17+
export type TraitField = { traits: string; nullable: boolean };
18+
export type FieldSpec = ScopeField | TraitField;
1719
export type ScopeTable = Record<string, ScopeField>;
1820

1921
export type Scope = {

packages/2-sql/4-lanes/sql-builder/test/fixtures/generated/contract.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { Interval } from '@prisma-next/adapter-postgres/codec-types';
1616
import type { CodecTypes as PgVectorTypes } from '@prisma-next/extension-pgvector/codec-types';
1717
import type { Vector } from '@prisma-next/extension-pgvector/codec-types';
1818
import type { OperationTypes as PgVectorOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
19+
import type { QueryOperationTypes as PgAdapterQueryOps } from '@prisma-next/adapter-postgres/operation-types';
1920
import type { QueryOperationTypes as PgVectorQueryOperationTypes } from '@prisma-next/extension-pgvector/operation-types';
2021

2122
import type {
@@ -39,7 +40,7 @@ export type ProfileHash =
3940
export type CodecTypes = PgTypes & PgVectorTypes;
4041
export type OperationTypes = PgVectorOperationTypes;
4142
export type LaneCodecTypes = CodecTypes;
42-
export type QueryOperationTypes = PgVectorQueryOperationTypes;
43+
export type QueryOperationTypes = PgAdapterQueryOps & PgVectorQueryOperationTypes;
4344
type DefaultLiteralValue<CodecId extends string, _Encoded> = CodecId extends keyof CodecTypes
4445
? CodecTypes[CodecId]['output']
4546
: _Encoded;

packages/2-sql/4-lanes/sql-builder/test/integration/extension-functions.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import { describe, expect, it } from 'vitest';
22
import { setupIntegrationTest } from './setup';
33

4+
describe('integration: ilike (adapter operation)', () => {
5+
const { db, runtime } = setupIntegrationTest();
6+
7+
it('ilike filters case-insensitively in WHERE', async () => {
8+
const rows = await runtime().execute(
9+
db()
10+
.users.select('id', 'name')
11+
.where((f, fns) => fns.ilike(f.name, '%alice%'))
12+
.build(),
13+
);
14+
expect(rows).toHaveLength(1);
15+
expect(rows[0]!.name).toBe('Alice');
16+
});
17+
18+
it('ilike returns no rows when pattern does not match', async () => {
19+
const rows = await runtime().execute(
20+
db()
21+
.users.select('id')
22+
.where((f, fns) => fns.ilike(f.name, '%zzz%'))
23+
.build(),
24+
);
25+
expect(rows).toHaveLength(0);
26+
});
27+
});
28+
429
describe('integration: extension functions', () => {
530
const { db, runtime } = setupIntegrationTest();
631

0 commit comments

Comments
 (0)