@prisma-next/sql-lane 0.3.0-dev.4 → 0.3.0-dev.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,434 @@
1
+ import type { ParamDescriptor } from '@prisma-next/contract/types';
2
+ import type { SqlContract, SqlStorage, StorageColumn } from '@prisma-next/sql-contract/types';
3
+ import type {
4
+ ColumnRef,
5
+ Direction,
6
+ IncludeAst,
7
+ IncludeRef,
8
+ JoinAst,
9
+ OperationExpr,
10
+ TableRef,
11
+ WhereExpr,
12
+ } from '@prisma-next/sql-relational-core/ast';
13
+ import {
14
+ createJoinOnBuilder,
15
+ createOrderByItem,
16
+ createSelectAst,
17
+ createTableRef,
18
+ } from '@prisma-next/sql-relational-core/ast';
19
+ import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
20
+ import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context';
21
+ import type {
22
+ AnyBinaryBuilder,
23
+ AnyOrderBuilder,
24
+ AnyUnaryBuilder,
25
+ BinaryBuilder,
26
+ BuildOptions,
27
+ InferNestedProjectionRow,
28
+ JoinOnBuilder,
29
+ JoinOnPredicate,
30
+ NestedProjection,
31
+ OrderBuilder,
32
+ SqlBuilderOptions,
33
+ UnaryBuilder,
34
+ } from '@prisma-next/sql-relational-core/types';
35
+ import { isExpressionBuilder } from '@prisma-next/sql-relational-core/utils/guards';
36
+ import type { ProjectionInput } from '../types/internal';
37
+ import { checkIncludeCapabilities } from '../utils/capabilities';
38
+ import {
39
+ errorChildProjectionEmpty,
40
+ errorFromMustBeCalled,
41
+ errorIncludeAliasCollision,
42
+ errorLimitMustBeNonNegativeInteger,
43
+ errorMissingAlias,
44
+ errorSelectMustBeCalled,
45
+ errorSelfJoinNotSupported,
46
+ errorUnknownTable,
47
+ } from '../utils/errors';
48
+ import type { BuilderState, IncludeState, JoinState, ProjectionState } from '../utils/state';
49
+ import {
50
+ buildIncludeAst,
51
+ type IncludeChildBuilder,
52
+ IncludeChildBuilderImpl,
53
+ } from './include-builder';
54
+ import { buildJoinAst } from './join-builder';
55
+ import { buildMeta } from './plan';
56
+ import { buildWhereExpr } from './predicate-builder';
57
+ import { buildProjectionState } from './projection';
58
+
59
+ export class SelectBuilderImpl<
60
+ TContract extends SqlContract<SqlStorage> = SqlContract<SqlStorage>,
61
+ Row = unknown,
62
+ CodecTypes extends Record<string, { readonly output: unknown }> = Record<string, never>,
63
+ Includes extends Record<string, unknown> = Record<string, never>,
64
+ > {
65
+ private readonly contract: TContract;
66
+ private readonly codecTypes: CodecTypes;
67
+ private readonly context: ExecutionContext<TContract>;
68
+ private state: BuilderState = {};
69
+
70
+ constructor(options: SqlBuilderOptions<TContract>, state?: BuilderState) {
71
+ this.context = options.context;
72
+ this.contract = options.context.contract;
73
+ this.codecTypes = options.context.contract.mappings.codecTypes as CodecTypes;
74
+ if (state) {
75
+ this.state = state;
76
+ }
77
+ }
78
+
79
+ from(table: TableRef): SelectBuilderImpl<TContract, unknown, CodecTypes, Record<string, never>> {
80
+ return new SelectBuilderImpl<TContract, unknown, CodecTypes, Record<string, never>>(
81
+ {
82
+ context: this.context,
83
+ },
84
+ { ...this.state, from: table },
85
+ );
86
+ }
87
+
88
+ innerJoin(
89
+ table: TableRef,
90
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
91
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
92
+ return this._addJoin('inner', table, on);
93
+ }
94
+
95
+ leftJoin(
96
+ table: TableRef,
97
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
98
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
99
+ return this._addJoin('left', table, on);
100
+ }
101
+
102
+ rightJoin(
103
+ table: TableRef,
104
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
105
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
106
+ return this._addJoin('right', table, on);
107
+ }
108
+
109
+ fullJoin(
110
+ table: TableRef,
111
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
112
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
113
+ return this._addJoin('full', table, on);
114
+ }
115
+
116
+ includeMany<
117
+ ChildProjection extends NestedProjection,
118
+ ChildRow = InferNestedProjectionRow<ChildProjection, CodecTypes>,
119
+ AliasName extends string = string,
120
+ >(
121
+ childTable: TableRef,
122
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
123
+ childBuilder: (
124
+ child: IncludeChildBuilder<TContract, CodecTypes, unknown>,
125
+ ) => IncludeChildBuilder<TContract, CodecTypes, ChildRow>,
126
+ options?: { alias?: AliasName },
127
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes & { [K in AliasName]: ChildRow }> {
128
+ checkIncludeCapabilities(this.contract);
129
+
130
+ if (!this.contract.storage.tables[childTable.name]) {
131
+ errorUnknownTable(childTable.name);
132
+ }
133
+
134
+ const joinOnBuilder = createJoinOnBuilder();
135
+ const onPredicate = on(joinOnBuilder);
136
+
137
+ // Validate ON uses column equality
138
+ // TypeScript can't narrow ColumnBuilder properly, so we assert
139
+ const onLeft = onPredicate.left as { table: string; column: string };
140
+ const onRight = onPredicate.right as { table: string; column: string };
141
+ if (onLeft.table === onRight.table) {
142
+ errorSelfJoinNotSupported();
143
+ }
144
+
145
+ // Build child builder
146
+ const childBuilderImpl = new IncludeChildBuilderImpl<TContract, CodecTypes, unknown>(
147
+ this.contract,
148
+ this.codecTypes,
149
+ childTable,
150
+ );
151
+ const builtChild = childBuilder(
152
+ childBuilderImpl as IncludeChildBuilder<TContract, CodecTypes, unknown>,
153
+ );
154
+ const childState = (
155
+ builtChild as IncludeChildBuilderImpl<TContract, CodecTypes, ChildRow>
156
+ ).getState();
157
+
158
+ // Validate child projection is non-empty
159
+ if (childState.childProjection.aliases.length === 0) {
160
+ errorChildProjectionEmpty();
161
+ }
162
+
163
+ // Determine alias
164
+ const alias = options?.alias ?? childTable.name;
165
+
166
+ // Check for alias collisions with existing projection
167
+ if (this.state.projection) {
168
+ if (this.state.projection.aliases.includes(alias)) {
169
+ errorIncludeAliasCollision(alias, 'projection');
170
+ }
171
+ }
172
+
173
+ // Check for alias collisions with existing includes
174
+ const existingIncludes = this.state.includes ?? [];
175
+ if (existingIncludes.some((inc) => inc.alias === alias)) {
176
+ errorIncludeAliasCollision(alias, 'include');
177
+ }
178
+
179
+ const includeState: IncludeState = {
180
+ alias,
181
+ table: childTable,
182
+ on: onPredicate,
183
+ childProjection: childState.childProjection,
184
+ ...(childState.childWhere !== undefined ? { childWhere: childState.childWhere } : {}),
185
+ ...(childState.childOrderBy !== undefined ? { childOrderBy: childState.childOrderBy } : {}),
186
+ ...(childState.childLimit !== undefined ? { childLimit: childState.childLimit } : {}),
187
+ };
188
+
189
+ const newIncludes = [...existingIncludes, includeState];
190
+
191
+ // Type-level: Update Includes map with new include
192
+ // The AliasName generic parameter is inferred from options.alias, allowing TypeScript
193
+ // to track include definitions across multiple includeMany() calls and infer correct
194
+ // array types when select() includes boolean true for include references
195
+ type NewIncludes = Includes & { [K in AliasName]: ChildRow };
196
+
197
+ return new SelectBuilderImpl<TContract, Row, CodecTypes, NewIncludes>(
198
+ {
199
+ context: this.context,
200
+ },
201
+ { ...this.state, includes: newIncludes },
202
+ );
203
+ }
204
+
205
+ private _addJoin(
206
+ joinType: 'inner' | 'left' | 'right' | 'full',
207
+ table: TableRef,
208
+ on: (on: JoinOnBuilder) => JoinOnPredicate,
209
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
210
+ const fromTable = this.ensureFrom();
211
+
212
+ if (!this.contract.storage.tables[table.name]) {
213
+ errorUnknownTable(table.name);
214
+ }
215
+
216
+ if (table.name === fromTable.name) {
217
+ errorSelfJoinNotSupported();
218
+ }
219
+
220
+ const joinOnBuilder = createJoinOnBuilder();
221
+ const onPredicate = on(joinOnBuilder);
222
+
223
+ const joinState: JoinState = {
224
+ joinType,
225
+ table,
226
+ on: onPredicate,
227
+ };
228
+
229
+ const existingJoins = this.state.joins ?? [];
230
+ const newJoins = [...existingJoins, joinState];
231
+
232
+ return new SelectBuilderImpl<TContract, Row, CodecTypes, Includes>(
233
+ {
234
+ context: this.context,
235
+ },
236
+ { ...this.state, joins: newJoins },
237
+ );
238
+ }
239
+
240
+ where(
241
+ expr: AnyBinaryBuilder | AnyUnaryBuilder,
242
+ ): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
243
+ return new SelectBuilderImpl<TContract, Row, CodecTypes, Includes>(
244
+ {
245
+ context: this.context,
246
+ },
247
+ { ...this.state, where: expr },
248
+ );
249
+ }
250
+
251
+ select<P extends ProjectionInput>(
252
+ projection: P,
253
+ ): SelectBuilderImpl<
254
+ TContract,
255
+ InferNestedProjectionRow<P, CodecTypes, Includes>,
256
+ CodecTypes,
257
+ Includes
258
+ > {
259
+ const table = this.ensureFrom();
260
+ const projectionState = buildProjectionState(table, projection, this.state.includes);
261
+
262
+ return new SelectBuilderImpl<
263
+ TContract,
264
+ InferNestedProjectionRow<P, CodecTypes, Includes>,
265
+ CodecTypes,
266
+ Includes
267
+ >(
268
+ {
269
+ context: this.context,
270
+ },
271
+ { ...this.state, projection: projectionState },
272
+ );
273
+ }
274
+
275
+ orderBy(order: AnyOrderBuilder): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
276
+ return new SelectBuilderImpl<TContract, Row, CodecTypes, Includes>(
277
+ {
278
+ context: this.context,
279
+ },
280
+ { ...this.state, orderBy: order },
281
+ );
282
+ }
283
+
284
+ limit(count: number): SelectBuilderImpl<TContract, Row, CodecTypes, Includes> {
285
+ if (!Number.isInteger(count) || count < 0) {
286
+ errorLimitMustBeNonNegativeInteger();
287
+ }
288
+
289
+ return new SelectBuilderImpl<TContract, Row, CodecTypes, Includes>(
290
+ {
291
+ context: this.context,
292
+ },
293
+ { ...this.state, limit: count },
294
+ );
295
+ }
296
+
297
+ build(options?: BuildOptions): SqlQueryPlan<Row> {
298
+ const table = this.ensureFrom();
299
+ const projection = this.ensureProjection();
300
+
301
+ const paramsMap = (options?.params ?? {}) as Record<string, unknown>;
302
+ const contractTable = this.contract.storage.tables[table.name];
303
+
304
+ if (!contractTable) {
305
+ errorUnknownTable(table.name);
306
+ }
307
+
308
+ const paramDescriptors: ParamDescriptor[] = [];
309
+ const paramValues: unknown[] = [];
310
+ const paramCodecs: Record<string, string> = {};
311
+
312
+ const whereResult = this.state.where
313
+ ? buildWhereExpr(this.contract, this.state.where, paramsMap, paramDescriptors, paramValues)
314
+ : undefined;
315
+ const whereExpr = whereResult?.expr;
316
+
317
+ if (whereResult?.codecId && whereResult.paramName) {
318
+ paramCodecs[whereResult.paramName] = whereResult.codecId;
319
+ }
320
+
321
+ const orderByClause = this.state.orderBy
322
+ ? (() => {
323
+ const orderBy = this.state.orderBy as OrderBuilder<string, StorageColumn, unknown>;
324
+ // orderBy.expr is already an Expression (ColumnRef or OperationExpr)
325
+ return [createOrderByItem(orderBy.expr, orderBy.dir)];
326
+ })()
327
+ : undefined;
328
+
329
+ const joins = this.state.joins?.map((join) => buildJoinAst(join));
330
+
331
+ const includes = this.state.includes?.map((include) =>
332
+ buildIncludeAst(include, this.contract, paramsMap, paramDescriptors, paramValues),
333
+ );
334
+
335
+ // Build projection with support for includeRef and OperationExpr
336
+ const projectEntries: Array<{ alias: string; expr: ColumnRef | IncludeRef | OperationExpr }> =
337
+ [];
338
+ for (let i = 0; i < projection.aliases.length; i++) {
339
+ const alias = projection.aliases[i];
340
+ if (!alias) {
341
+ errorMissingAlias(i);
342
+ }
343
+ const column = projection.columns[i];
344
+
345
+ // Check if this alias matches an include alias first
346
+ // Include placeholders have null columns
347
+ const matchingInclude = this.state.includes?.find((inc) => inc.alias === alias);
348
+ if (matchingInclude) {
349
+ // This is an include reference - column can be null for placeholders
350
+ projectEntries.push({
351
+ alias,
352
+ expr: { kind: 'includeRef', alias },
353
+ });
354
+ } else if (column && isExpressionBuilder(column)) {
355
+ // This is an ExpressionBuilder (operation result) - use its expr
356
+ projectEntries.push({
357
+ alias,
358
+ expr: column.expr,
359
+ });
360
+ } else if (column) {
361
+ // This is a regular ColumnBuilder - use toExpr() to get ColumnRef
362
+ const columnRef = column.toExpr();
363
+ projectEntries.push({
364
+ alias,
365
+ expr: columnRef,
366
+ });
367
+ }
368
+ }
369
+
370
+ const ast = createSelectAst({
371
+ from: createTableRef(table.name),
372
+ joins,
373
+ includes,
374
+ project: projectEntries,
375
+ where: whereExpr,
376
+ orderBy: orderByClause,
377
+ limit: this.state.limit,
378
+ } as {
379
+ from: TableRef;
380
+ joins?: ReadonlyArray<JoinAst>;
381
+ includes?: ReadonlyArray<IncludeAst>;
382
+ project: ReadonlyArray<{ alias: string; expr: ColumnRef | IncludeRef | OperationExpr }>;
383
+ where?: WhereExpr;
384
+ orderBy?: ReadonlyArray<{ expr: ColumnRef | OperationExpr; dir: Direction }>;
385
+ limit?: number;
386
+ });
387
+
388
+ const planMeta = buildMeta({
389
+ contract: this.contract,
390
+ table,
391
+ projection,
392
+ joins: this.state.joins,
393
+ includes: this.state.includes,
394
+ paramDescriptors,
395
+ paramCodecs,
396
+ where: this.state.where,
397
+ orderBy: this.state.orderBy,
398
+ } as {
399
+ contract: SqlContract<SqlStorage>;
400
+ table: TableRef;
401
+ projection: ProjectionState;
402
+ joins?: ReadonlyArray<JoinState>;
403
+ includes?: ReadonlyArray<IncludeState>;
404
+ where?: BinaryBuilder | UnaryBuilder;
405
+ orderBy?: AnyOrderBuilder;
406
+ paramDescriptors: ParamDescriptor[];
407
+ paramCodecs?: Record<string, string>;
408
+ });
409
+
410
+ const queryPlan: SqlQueryPlan<Row> = Object.freeze({
411
+ ast,
412
+ params: paramValues,
413
+ meta: planMeta,
414
+ });
415
+
416
+ return queryPlan;
417
+ }
418
+
419
+ private ensureFrom() {
420
+ if (!this.state.from) {
421
+ errorFromMustBeCalled();
422
+ }
423
+
424
+ return this.state.from;
425
+ }
426
+
427
+ private ensureProjection() {
428
+ if (!this.state.projection) {
429
+ errorSelectMustBeCalled();
430
+ }
431
+
432
+ return this.state.projection;
433
+ }
434
+ }
@@ -0,0 +1,42 @@
1
+ import type { ParamDescriptor } from '@prisma-next/contract/types';
2
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
3
+ import type { TableRef } from '@prisma-next/sql-relational-core/ast';
4
+ import type {
5
+ AnyBinaryBuilder,
6
+ AnyExpressionSource,
7
+ AnyOrderBuilder,
8
+ AnyUnaryBuilder,
9
+ NestedProjection,
10
+ } from '@prisma-next/sql-relational-core/types';
11
+ import type { ProjectionState } from '../utils/state';
12
+
13
+ export type ProjectionInput = Record<string, AnyExpressionSource | boolean | NestedProjection>;
14
+
15
+ export interface MetaBuildArgs {
16
+ readonly contract: SqlContract<SqlStorage>;
17
+ readonly table: TableRef;
18
+ readonly projection: ProjectionState;
19
+ readonly joins?: ReadonlyArray<{
20
+ readonly joinType: 'inner' | 'left' | 'right' | 'full';
21
+ readonly table: TableRef;
22
+ readonly on: {
23
+ readonly left: unknown;
24
+ readonly right: unknown;
25
+ };
26
+ }>;
27
+ readonly includes?: ReadonlyArray<{
28
+ readonly alias: string;
29
+ readonly table: TableRef;
30
+ readonly on: {
31
+ readonly left: unknown;
32
+ readonly right: unknown;
33
+ };
34
+ readonly childProjection: ProjectionState;
35
+ readonly childWhere?: AnyBinaryBuilder | AnyUnaryBuilder;
36
+ readonly childOrderBy?: AnyOrderBuilder;
37
+ }>;
38
+ readonly where?: AnyBinaryBuilder | AnyUnaryBuilder;
39
+ readonly orderBy?: AnyOrderBuilder;
40
+ readonly paramDescriptors: ParamDescriptor[];
41
+ readonly paramCodecs?: Record<string, string>;
42
+ }
@@ -0,0 +1,36 @@
1
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
2
+ import type { TableRef } from '@prisma-next/sql-relational-core/ast';
3
+ import type { ParamPlaceholder, RawFactory } from '@prisma-next/sql-relational-core/types';
4
+ import type { DeleteBuilder, InsertBuilder, UpdateBuilder } from '../sql/mutation-builder';
5
+ import type { SelectBuilderImpl } from '../sql/select-builder';
6
+
7
+ export type { TableRef } from '@prisma-next/sql-relational-core/ast';
8
+ export type {
9
+ AnyColumnBuilder,
10
+ BuildOptions,
11
+ InferReturningRow,
12
+ ParamPlaceholder,
13
+ RawFactory,
14
+ SqlBuilderOptions,
15
+ } from '@prisma-next/sql-relational-core/types';
16
+
17
+ export type SelectBuilder<
18
+ TContract extends SqlContract<SqlStorage> = SqlContract<SqlStorage>,
19
+ Row = unknown,
20
+ CodecTypes extends Record<string, { readonly output: unknown }> = Record<string, never>,
21
+ Includes extends Record<string, unknown> = Record<string, never>,
22
+ > = SelectBuilderImpl<TContract, Row, CodecTypes, Includes> & {
23
+ readonly raw: RawFactory;
24
+ insert(
25
+ table: TableRef,
26
+ values: Record<string, ParamPlaceholder>,
27
+ ): InsertBuilder<TContract, CodecTypes>;
28
+ update(
29
+ table: TableRef,
30
+ set: Record<string, ParamPlaceholder>,
31
+ ): UpdateBuilder<TContract, CodecTypes>;
32
+ delete(table: TableRef): DeleteBuilder<TContract, CodecTypes>;
33
+ };
34
+
35
+ export type { IncludeChildBuilder } from '../sql/include-builder';
36
+ export type { DeleteBuilder, InsertBuilder, UpdateBuilder } from '../sql/mutation-builder';
@@ -0,0 +1,34 @@
1
+ import { planInvalid } from '@prisma-next/plan';
2
+ import type { AnyColumnBuilder } from '@prisma-next/sql-relational-core/types';
3
+
4
+ /**
5
+ * Asserts that a ColumnBuilder has table and column properties.
6
+ */
7
+ export function assertColumnBuilder(col: unknown, context: string): AnyColumnBuilder {
8
+ if (
9
+ typeof col === 'object' &&
10
+ col !== null &&
11
+ 'table' in col &&
12
+ 'column' in col &&
13
+ typeof (col as { table: unknown }).table === 'string' &&
14
+ typeof (col as { column: unknown }).column === 'string'
15
+ ) {
16
+ return col as AnyColumnBuilder;
17
+ }
18
+ throw planInvalid(`ColumnBuilder missing table/column in ${context}`);
19
+ }
20
+
21
+ /**
22
+ * Asserts that a JoinOnPredicate has valid left and right columns.
23
+ */
24
+ export function assertJoinOnPredicate(on: {
25
+ left?: { table?: string; column?: string };
26
+ right?: { table?: string; column?: string };
27
+ }): asserts on is {
28
+ left: { table: string; column: string };
29
+ right: { table: string; column: string };
30
+ } {
31
+ if (!on.left?.table || !on.left?.column || !on.right?.table || !on.right?.column) {
32
+ throw planInvalid('JoinOnPredicate missing required table/column properties');
33
+ }
34
+ }
@@ -0,0 +1,39 @@
1
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
2
+ import {
3
+ errorIncludeCapabilitiesNotTrue,
4
+ errorIncludeRequiresCapabilities,
5
+ errorReturningCapabilityNotTrue,
6
+ errorReturningRequiresCapability,
7
+ } from './errors';
8
+
9
+ export function checkIncludeCapabilities(contract: SqlContract<SqlStorage>): void {
10
+ const target = contract.target;
11
+ const contractCapabilities = contract.capabilities;
12
+ const declaredTargetCapabilities = contractCapabilities?.[target];
13
+
14
+ if (!contractCapabilities || !declaredTargetCapabilities) {
15
+ errorIncludeRequiresCapabilities(target);
16
+ }
17
+
18
+ if (
19
+ declaredTargetCapabilities['lateral'] !== true ||
20
+ declaredTargetCapabilities['jsonAgg'] !== true
21
+ ) {
22
+ errorIncludeCapabilitiesNotTrue(target, {
23
+ lateral: declaredTargetCapabilities['lateral'],
24
+ jsonAgg: declaredTargetCapabilities['jsonAgg'],
25
+ });
26
+ }
27
+ }
28
+
29
+ export function checkReturningCapability(contract: SqlContract<SqlStorage>): void {
30
+ const target = contract.target;
31
+ const capabilities = contract.capabilities;
32
+ if (!capabilities || !capabilities[target]) {
33
+ errorReturningRequiresCapability(target);
34
+ }
35
+ const targetCapabilities = capabilities[target];
36
+ if (targetCapabilities['returning'] !== true) {
37
+ errorReturningCapabilityNotTrue(target, targetCapabilities['returning']);
38
+ }
39
+ }