@prisma-next/sql-lane 0.3.0-pr.95.1 → 0.3.0-pr.96.1

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.
package/src/sql/plan.ts CHANGED
@@ -1,72 +1,42 @@
1
1
  import type { PlanMeta } from '@prisma-next/contract/types';
2
- import type { Expression } from '@prisma-next/sql-relational-core/ast';
2
+ import type { OperationExpr } from '@prisma-next/sql-relational-core/ast';
3
3
  import { compact } from '@prisma-next/sql-relational-core/ast';
4
- import type { AnyExpressionSource } from '@prisma-next/sql-relational-core/types';
4
+ import type { AnyColumnBuilder } from '@prisma-next/sql-relational-core/types';
5
5
  import {
6
6
  collectColumnRefs,
7
+ getColumnInfo,
8
+ getOperationExpr,
7
9
  isColumnBuilder,
8
- isExpressionBuilder,
9
10
  isOperationExpr,
10
11
  } from '@prisma-next/sql-relational-core/utils/guards';
11
12
  import type { MetaBuildArgs } from '../types/internal';
12
13
  import { assertColumnBuilder } from '../utils/assertions';
13
14
  import { errorMissingColumnForAlias } from '../utils/errors';
14
15
 
15
- /**
16
- * Extracts column references from an ExpressionSource (ColumnBuilder or ExpressionBuilder).
17
- */
18
- function collectRefsFromExpressionSource(
19
- source: AnyExpressionSource,
20
- refsColumns: Map<string, { table: string; column: string }>,
21
- ): void {
22
- if (isExpressionBuilder(source)) {
23
- // ExpressionBuilder has an OperationExpr - collect all column refs
24
- const allRefs = collectColumnRefs(source.expr);
25
- for (const ref of allRefs) {
26
- refsColumns.set(`${ref.table}.${ref.column}`, {
27
- table: ref.table,
28
- column: ref.column,
29
- });
30
- }
31
- } else if (isColumnBuilder(source)) {
32
- // ColumnBuilder - use table and column directly
33
- const col = source as unknown as { table: string; column: string };
34
- refsColumns.set(`${col.table}.${col.column}`, {
35
- table: col.table,
36
- column: col.column,
37
- });
38
- }
39
- }
40
-
41
- /**
42
- * Extracts column references from an Expression (AST node).
43
- */
44
- function collectRefsFromExpression(
45
- expr: Expression,
46
- refsColumns: Map<string, { table: string; column: string }>,
47
- ): void {
48
- if (isOperationExpr(expr)) {
49
- const allRefs = collectColumnRefs(expr);
50
- for (const ref of allRefs) {
51
- refsColumns.set(`${ref.table}.${ref.column}`, {
52
- table: ref.table,
53
- column: ref.column,
54
- });
55
- }
56
- } else if (expr.kind === 'col') {
57
- refsColumns.set(`${expr.table}.${expr.column}`, {
58
- table: expr.table,
59
- column: expr.column,
60
- });
61
- }
62
- }
63
-
64
16
  export function buildMeta(args: MetaBuildArgs): PlanMeta {
65
17
  const refsColumns = new Map<string, { table: string; column: string }>();
66
18
  const refsTables = new Set<string>([args.table.name]);
67
19
 
68
20
  for (const column of args.projection.columns) {
69
- collectRefsFromExpressionSource(column, refsColumns);
21
+ // Skip null columns (include placeholders)
22
+ if (!column) {
23
+ continue;
24
+ }
25
+ const operationExpr = getOperationExpr(column);
26
+ if (operationExpr) {
27
+ const allRefs = collectColumnRefs(operationExpr);
28
+ for (const ref of allRefs) {
29
+ refsColumns.set(`${ref.table}.${ref.column}`, {
30
+ table: ref.table,
31
+ column: ref.column,
32
+ });
33
+ }
34
+ } else {
35
+ refsColumns.set(`${column.table}.${column.column}`, {
36
+ table: column.table,
37
+ column: column.column,
38
+ });
39
+ }
70
40
  }
71
41
 
72
42
  if (args.joins) {
@@ -111,27 +81,42 @@ export function buildMeta(args: MetaBuildArgs): PlanMeta {
111
81
  }
112
82
  // Add child WHERE columns if present
113
83
  if (include.childWhere) {
114
- // childWhere.left is Expression (already converted at builder creation time)
115
- collectRefsFromExpression(include.childWhere.left, refsColumns);
116
- // Handle right side of child WHERE clause - can be ParamPlaceholder or ExpressionSource
84
+ const colInfo = getColumnInfo(include.childWhere.left);
85
+ refsColumns.set(`${colInfo.table}.${colInfo.column}`, {
86
+ table: colInfo.table,
87
+ column: colInfo.column,
88
+ });
89
+ // Handle right side of child WHERE clause
117
90
  const childWhereRight = include.childWhere.right;
118
- if (isColumnBuilder(childWhereRight) || isExpressionBuilder(childWhereRight)) {
119
- collectRefsFromExpressionSource(childWhereRight, refsColumns);
91
+ if (isColumnBuilder(childWhereRight)) {
92
+ const rightColInfo = getColumnInfo(childWhereRight);
93
+ refsColumns.set(`${rightColInfo.table}.${rightColInfo.column}`, {
94
+ table: rightColInfo.table,
95
+ column: rightColInfo.column,
96
+ });
120
97
  }
121
98
  }
122
99
  // Add child ORDER BY columns if present
123
100
  if (include.childOrderBy) {
124
- // childOrderBy.expr is Expression (already converted at builder creation time)
125
- collectRefsFromExpression(include.childOrderBy.expr, refsColumns);
101
+ const orderBy = include.childOrderBy as unknown as {
102
+ expr?: AnyColumnBuilder | OperationExpr;
103
+ };
104
+ if (orderBy.expr) {
105
+ const colInfo = getColumnInfo(orderBy.expr);
106
+ refsColumns.set(`${colInfo.table}.${colInfo.column}`, {
107
+ table: colInfo.table,
108
+ column: colInfo.column,
109
+ });
110
+ }
126
111
  }
127
112
  }
128
113
  }
129
114
 
130
115
  if (args.where) {
131
- // args.where.left is Expression (already converted at builder creation time)
132
- const leftExpr: Expression = args.where.left;
133
- if (isOperationExpr(leftExpr)) {
134
- const allRefs = collectColumnRefs(leftExpr);
116
+ const whereLeft = args.where.left;
117
+ // Check if whereLeft is an OperationExpr directly (not wrapped in ColumnBuilder)
118
+ if (isOperationExpr(whereLeft)) {
119
+ const allRefs = collectColumnRefs(whereLeft);
135
120
  for (const ref of allRefs) {
136
121
  refsColumns.set(`${ref.table}.${ref.column}`, {
137
122
  table: ref.table,
@@ -139,37 +124,56 @@ export function buildMeta(args: MetaBuildArgs): PlanMeta {
139
124
  });
140
125
  }
141
126
  } else {
142
- // leftExpr is ColumnRef
143
- refsColumns.set(`${leftExpr.table}.${leftExpr.column}`, {
144
- table: leftExpr.table,
145
- column: leftExpr.column,
146
- });
127
+ // Check if whereLeft is a ColumnBuilder with an _operationExpr property
128
+ const operationExpr = (whereLeft as { _operationExpr?: OperationExpr })._operationExpr;
129
+ if (operationExpr) {
130
+ const allRefs = collectColumnRefs(operationExpr);
131
+ for (const ref of allRefs) {
132
+ refsColumns.set(`${ref.table}.${ref.column}`, {
133
+ table: ref.table,
134
+ column: ref.column,
135
+ });
136
+ }
137
+ } else {
138
+ const colBuilder = assertColumnBuilder(whereLeft, 'where clause must be a ColumnBuilder');
139
+ refsColumns.set(`${colBuilder.table}.${colBuilder.column}`, {
140
+ table: colBuilder.table,
141
+ column: colBuilder.column,
142
+ });
143
+ }
147
144
  }
148
145
 
149
- // Handle right side of WHERE clause - can be ParamPlaceholder or AnyExpressionSource
146
+ // Handle right side of WHERE clause - can be ParamPlaceholder or AnyColumnBuilder
150
147
  const whereRight = args.where.right;
151
- if (isColumnBuilder(whereRight) || isExpressionBuilder(whereRight)) {
152
- collectRefsFromExpressionSource(whereRight, refsColumns);
148
+ if (isColumnBuilder(whereRight)) {
149
+ const colInfo = getColumnInfo(whereRight);
150
+ refsColumns.set(`${colInfo.table}.${colInfo.column}`, {
151
+ table: colInfo.table,
152
+ column: colInfo.column,
153
+ });
153
154
  }
154
155
  }
155
156
 
156
157
  if (args.orderBy) {
157
- // args.orderBy.expr is Expression (already converted at builder creation time)
158
- const orderByExpr: Expression = args.orderBy.expr;
159
- if (isOperationExpr(orderByExpr)) {
160
- const allRefs = collectColumnRefs(orderByExpr);
161
- for (const ref of allRefs) {
162
- refsColumns.set(`${ref.table}.${ref.column}`, {
163
- table: ref.table,
164
- column: ref.column,
158
+ const orderBy = args.orderBy as unknown as {
159
+ expr?: AnyColumnBuilder | OperationExpr;
160
+ };
161
+ const orderByExpr = orderBy.expr;
162
+ if (orderByExpr) {
163
+ if (isOperationExpr(orderByExpr)) {
164
+ const allRefs = collectColumnRefs(orderByExpr);
165
+ for (const ref of allRefs) {
166
+ refsColumns.set(`${ref.table}.${ref.column}`, {
167
+ table: ref.table,
168
+ column: ref.column,
169
+ });
170
+ }
171
+ } else {
172
+ refsColumns.set(`${orderByExpr.table}.${orderByExpr.column}`, {
173
+ table: orderByExpr.table,
174
+ column: orderByExpr.column,
165
175
  });
166
176
  }
167
- } else {
168
- // orderByExpr is ColumnRef
169
- refsColumns.set(`${orderByExpr.table}.${orderByExpr.column}`, {
170
- table: orderByExpr.table,
171
- column: orderByExpr.column,
172
- });
173
177
  }
174
178
  }
175
179
 
@@ -187,18 +191,13 @@ export function buildMeta(args: MetaBuildArgs): PlanMeta {
187
191
  // This shouldn't happen if projection building is correct, but handle gracefully
188
192
  errorMissingColumnForAlias(alias, index);
189
193
  }
190
- // Check if column is an ExpressionBuilder (operation result)
191
- if (isExpressionBuilder(column)) {
192
- return [alias, `operation:${column.expr.method}`];
193
- }
194
- // column is ColumnBuilder
195
- const col = column as unknown as { table?: string; column?: string };
196
- if (!col.table || !col.column) {
197
- // This is a placeholder column for an include - skip it
198
- return [alias, `include:${alias}`];
194
+
195
+ const operationExpr = getOperationExpr(column);
196
+ if (operationExpr) {
197
+ return [alias, `operation:${operationExpr.method}`];
199
198
  }
200
199
 
201
- return [alias, `${col.table}.${col.column}`];
200
+ return [alias, `${column.table}.${column.column}`];
202
201
  }),
203
202
  );
204
203
 
@@ -214,15 +213,15 @@ export function buildMeta(args: MetaBuildArgs): PlanMeta {
214
213
  if (!column) {
215
214
  continue;
216
215
  }
217
- if (isExpressionBuilder(column)) {
218
- const operationExpr = column.expr;
216
+ const operationExpr = (column as { _operationExpr?: OperationExpr })._operationExpr;
217
+ if (operationExpr) {
219
218
  if (operationExpr.returns.kind === 'typeId') {
220
219
  projectionTypes[alias] = operationExpr.returns.type;
221
220
  } else if (operationExpr.returns.kind === 'builtin') {
222
221
  projectionTypes[alias] = operationExpr.returns.type;
223
222
  }
224
223
  } else {
225
- // column is ColumnBuilder
224
+ // TypeScript can't narrow ColumnBuilder properly
226
225
  const col = column as unknown as { columnMeta?: { codecId: string } };
227
226
  const columnMeta = col.columnMeta;
228
227
  const codecId = columnMeta?.codecId;
@@ -244,14 +243,14 @@ export function buildMeta(args: MetaBuildArgs): PlanMeta {
244
243
  if (!column) {
245
244
  continue;
246
245
  }
247
- if (isExpressionBuilder(column)) {
248
- const operationExpr = column.expr;
246
+ const operationExpr = (column as { _operationExpr?: OperationExpr })._operationExpr;
247
+ if (operationExpr) {
249
248
  if (operationExpr.returns.kind === 'typeId') {
250
249
  projectionCodecs[alias] = operationExpr.returns.type;
251
250
  }
252
251
  } else {
253
252
  // Use columnMeta.codecId directly as typeId (already canonicalized)
254
- // column is ColumnBuilder
253
+ // TypeScript can't narrow ColumnBuilder properly
255
254
  const col = column as unknown as { columnMeta?: { codecId: string } };
256
255
  const columnMeta = col.columnMeta;
257
256
  const codecId = columnMeta?.codecId;
@@ -1,11 +1,21 @@
1
1
  import type { ParamDescriptor } from '@prisma-next/contract/types';
2
2
  import type { SqlContract, SqlStorage, StorageColumn } from '@prisma-next/sql-contract/types';
3
- import type { BinaryExpr, Expression, ParamRef } from '@prisma-next/sql-relational-core/ast';
4
- import { createBinaryExpr, createParamRef } from '@prisma-next/sql-relational-core/ast';
3
+ import type {
4
+ BinaryExpr,
5
+ ColumnRef,
6
+ OperationExpr,
7
+ ParamRef,
8
+ } from '@prisma-next/sql-relational-core/ast';
9
+ import {
10
+ createBinaryExpr,
11
+ createColumnRef,
12
+ createParamRef,
13
+ } from '@prisma-next/sql-relational-core/ast';
5
14
  import type { BinaryBuilder, ParamPlaceholder } from '@prisma-next/sql-relational-core/types';
6
15
  import {
16
+ getColumnInfo,
17
+ getOperationExpr,
7
18
  isColumnBuilder,
8
- isExpressionBuilder,
9
19
  isParamPlaceholder,
10
20
  } from '@prisma-next/sql-relational-core/utils/guards';
11
21
  import {
@@ -28,28 +38,20 @@ export function buildWhereExpr(
28
38
  descriptors: ParamDescriptor[],
29
39
  values: unknown[],
30
40
  ): BuildWhereExprResult {
31
- let leftExpr: Expression;
41
+ let leftExpr: ColumnRef | OperationExpr;
32
42
  let codecId: string | undefined;
33
- let rightExpr: Expression | ParamRef;
43
+ let rightExpr: ColumnRef | ParamRef;
34
44
  let paramName: string;
35
45
 
36
- // Validate where.left is a valid Expression (col or operation)
37
- const validExpressionKinds = ['col', 'operation'];
38
- if (
39
- !where.left ||
40
- typeof where.left !== 'object' ||
41
- !validExpressionKinds.includes((where.left as { kind?: string }).kind ?? '')
42
- ) {
43
- errorFailedToBuildWhereClause();
44
- }
45
-
46
- // where.left is an Expression (already converted at builder creation time)
47
- // It could be a ColumnRef or OperationExpr
48
- leftExpr = where.left;
46
+ // Check if where.left is an OperationExpr directly (from operation.eq())
47
+ // or a ColumnBuilder with _operationExpr property
48
+ const operationExpr = getOperationExpr(where.left);
49
+ if (operationExpr) {
50
+ leftExpr = operationExpr;
51
+ } else if (isColumnBuilder(where.left)) {
52
+ // where.left is a ColumnBuilder - use proper type narrowing
53
+ const { table, column } = getColumnInfo(where.left);
49
54
 
50
- // If the left expression is a column reference, extract codecId for param descriptors
51
- if (leftExpr.kind === 'col') {
52
- const { table, column } = leftExpr;
53
55
  const contractTable = contract.storage.tables[table];
54
56
  if (!contractTable) {
55
57
  errorUnknownTable(table);
@@ -61,9 +63,13 @@ export function buildWhereExpr(
61
63
  }
62
64
 
63
65
  codecId = columnMeta.codecId;
66
+ leftExpr = createColumnRef(table, column);
67
+ } else {
68
+ // where.left is neither OperationExpr nor ColumnBuilder - invalid state
69
+ errorFailedToBuildWhereClause();
64
70
  }
65
71
 
66
- // Handle where.right - can be ParamPlaceholder or ExpressionSource
72
+ // Handle where.right - can be ParamPlaceholder or AnyColumnBuilder
67
73
  if (isParamPlaceholder(where.right)) {
68
74
  // Handle param placeholder (existing logic)
69
75
  const placeholder: ParamPlaceholder = where.right;
@@ -76,9 +82,9 @@ export function buildWhereExpr(
76
82
  const value = paramsMap[paramName];
77
83
  const index = values.push(value);
78
84
 
79
- // Construct descriptor directly from validated StorageColumn if left is a column
80
- if (leftExpr.kind === 'col') {
81
- const { table, column } = leftExpr;
85
+ // Construct descriptor directly from validated StorageColumn
86
+ if (isColumnBuilder(where.left)) {
87
+ const { table, column } = getColumnInfo(where.left);
82
88
  const contractTable = contract.storage.tables[table];
83
89
  const columnMeta = contractTable?.columns[column];
84
90
  if (columnMeta) {
@@ -94,28 +100,25 @@ export function buildWhereExpr(
94
100
  }
95
101
 
96
102
  rightExpr = createParamRef(index, paramName);
97
- } else if (isColumnBuilder(where.right) || isExpressionBuilder(where.right)) {
98
- // Handle ExpressionSource (ColumnBuilder or ExpressionBuilder) on the right
99
- rightExpr = where.right.toExpr();
103
+ } else if (isColumnBuilder(where.right)) {
104
+ // Handle column builder on the right
105
+ const { table, column } = getColumnInfo(where.right);
100
106
 
101
- // Validate column exists in contract if it's a ColumnRef
102
- if (rightExpr.kind === 'col') {
103
- const { table, column } = rightExpr;
104
- const contractTable = contract.storage.tables[table];
105
- if (!contractTable) {
106
- errorUnknownTable(table);
107
- }
107
+ const contractTable = contract.storage.tables[table];
108
+ if (!contractTable) {
109
+ errorUnknownTable(table);
110
+ }
108
111
 
109
- const columnMeta: StorageColumn | undefined = contractTable.columns[column];
110
- if (!columnMeta) {
111
- errorUnknownColumn(column, table);
112
- }
112
+ const columnMeta: StorageColumn | undefined = contractTable.columns[column];
113
+ if (!columnMeta) {
114
+ errorUnknownColumn(column, table);
113
115
  }
114
116
 
115
- // Use a placeholder paramName for expression references (not used for params)
117
+ rightExpr = createColumnRef(table, column);
118
+ // Use a placeholder paramName for column references (not used for params)
116
119
  paramName = '';
117
120
  } else {
118
- // where.right is neither ParamPlaceholder nor ExpressionSource - invalid state
121
+ // where.right is neither ParamPlaceholder nor ColumnBuilder - invalid state
119
122
  errorFailedToBuildWhereClause();
120
123
  }
121
124
 
@@ -1,6 +1,6 @@
1
1
  import type { TableRef } from '@prisma-next/sql-relational-core/ast';
2
- import type { AnyExpressionSource, NestedProjection } from '@prisma-next/sql-relational-core/types';
3
- import { isExpressionSource } from '@prisma-next/sql-relational-core/utils/guards';
2
+ import type { AnyColumnBuilder, NestedProjection } from '@prisma-next/sql-relational-core/types';
3
+ import { isColumnBuilder } from '@prisma-next/sql-relational-core/utils/guards';
4
4
  import type { ProjectionInput } from '../types/internal';
5
5
  import {
6
6
  errorAliasCollision,
@@ -47,14 +47,14 @@ export function flattenProjection(
47
47
  projection: NestedProjection,
48
48
  tracker: AliasTracker,
49
49
  currentPath: string[] = [],
50
- ): { aliases: string[]; columns: AnyExpressionSource[] } {
50
+ ): { aliases: string[]; columns: AnyColumnBuilder[] } {
51
51
  const aliases: string[] = [];
52
- const columns: AnyExpressionSource[] = [];
52
+ const columns: AnyColumnBuilder[] = [];
53
53
 
54
54
  for (const [key, value] of Object.entries(projection)) {
55
55
  const path = [...currentPath, key];
56
56
 
57
- if (isExpressionSource(value)) {
57
+ if (isColumnBuilder(value)) {
58
58
  const alias = tracker.register(path);
59
59
  aliases.push(alias);
60
60
  columns.push(value);
@@ -77,7 +77,7 @@ export function buildProjectionState(
77
77
  ): ProjectionState {
78
78
  const tracker = new AliasTracker();
79
79
  const aliases: string[] = [];
80
- const columns: AnyExpressionSource[] = [];
80
+ const columns: (AnyColumnBuilder | null)[] = [];
81
81
 
82
82
  for (const [key, value] of Object.entries(projection)) {
83
83
  if (value === true) {
@@ -86,17 +86,12 @@ export function buildProjectionState(
86
86
  if (!matchingInclude) {
87
87
  errorIncludeAliasNotFound(key);
88
88
  }
89
- // For include references, we track the alias but use a placeholder object
89
+ // For include references, we track the alias but use null as placeholder
90
90
  // The actual handling happens in AST building where we create includeRef
91
+ // Using null instead of invalid ColumnBuilder avoids code smell
91
92
  aliases.push(key);
92
- columns.push({
93
- kind: 'column',
94
- table: matchingInclude.table.name,
95
- column: '',
96
- columnMeta: { nativeType: 'jsonb', codecId: 'core/json@1', nullable: true },
97
- toExpr: () => ({ kind: 'col', table: matchingInclude.table.name, column: '' }),
98
- } as AnyExpressionSource);
99
- } else if (isExpressionSource(value)) {
93
+ columns.push(null);
94
+ } else if (isColumnBuilder(value)) {
100
95
  const alias = tracker.register([key]);
101
96
  aliases.push(alias);
102
97
  columns.push(value);
@@ -11,6 +11,7 @@ import type {
11
11
  TableRef,
12
12
  } from '@prisma-next/sql-relational-core/ast';
13
13
  import {
14
+ createColumnRef,
14
15
  createJoinOnBuilder,
15
16
  createOrderByItem,
16
17
  createSelectAst,
@@ -30,8 +31,9 @@ import type {
30
31
  OrderBuilder,
31
32
  SqlBuilderOptions,
32
33
  } from '@prisma-next/sql-relational-core/types';
33
- import { isExpressionBuilder } from '@prisma-next/sql-relational-core/utils/guards';
34
+ import { getOperationExpr } from '@prisma-next/sql-relational-core/utils/guards';
34
35
  import type { ProjectionInput } from '../types/internal';
36
+ import { assertColumnBuilder } from '../utils/assertions';
35
37
  import { checkIncludeCapabilities } from '../utils/capabilities';
36
38
  import {
37
39
  errorChildProjectionEmpty,
@@ -39,6 +41,7 @@ import {
39
41
  errorIncludeAliasCollision,
40
42
  errorLimitMustBeNonNegativeInteger,
41
43
  errorMissingAlias,
44
+ errorMissingColumnForAlias,
42
45
  errorSelectMustBeCalled,
43
46
  errorSelfJoinNotSupported,
44
47
  errorUnknownTable,
@@ -317,8 +320,15 @@ export class SelectBuilderImpl<
317
320
  const orderByClause = this.state.orderBy
318
321
  ? (() => {
319
322
  const orderBy = this.state.orderBy as OrderBuilder<string, StorageColumn, unknown>;
320
- // orderBy.expr is already an Expression (ColumnRef or OperationExpr)
321
- return [createOrderByItem(orderBy.expr, orderBy.dir)];
323
+ const orderExpr = orderBy.expr;
324
+ const operationExpr = getOperationExpr(orderExpr);
325
+ const expr: ColumnRef | OperationExpr = operationExpr
326
+ ? operationExpr
327
+ : (() => {
328
+ const colBuilder = orderExpr as { table: string; column: string };
329
+ return createColumnRef(colBuilder.table, colBuilder.column);
330
+ })();
331
+ return [createOrderByItem(expr, orderBy.dir)];
322
332
  })()
323
333
  : undefined;
324
334
 
@@ -347,19 +357,28 @@ export class SelectBuilderImpl<
347
357
  alias,
348
358
  expr: { kind: 'includeRef', alias },
349
359
  });
350
- } else if (column && isExpressionBuilder(column)) {
351
- // This is an ExpressionBuilder (operation result) - use its expr
352
- projectEntries.push({
353
- alias,
354
- expr: column.expr,
355
- });
356
- } else if (column) {
357
- // This is a regular ColumnBuilder - use toExpr() to get ColumnRef
358
- const columnRef = column.toExpr();
359
- projectEntries.push({
360
- alias,
361
- expr: columnRef,
362
- });
360
+ } else {
361
+ // Not an include - column must not be null
362
+ if (!column) {
363
+ errorMissingColumnForAlias(alias, i);
364
+ }
365
+ // Check if this column has an operation expression
366
+ const operationExpr = (column as { _operationExpr?: OperationExpr })._operationExpr;
367
+ if (operationExpr) {
368
+ projectEntries.push({
369
+ alias,
370
+ expr: operationExpr,
371
+ });
372
+ } else {
373
+ // This is a regular column
374
+ // TypeScript can't narrow ColumnBuilder properly
375
+ const col = column as { table: string; column: string };
376
+ assertColumnBuilder(col, 'projection column');
377
+ projectEntries.push({
378
+ alias,
379
+ expr: createColumnRef(col.table, col.column),
380
+ });
381
+ }
363
382
  }
364
383
  }
365
384
 
@@ -3,13 +3,13 @@ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
3
3
  import type { TableRef } from '@prisma-next/sql-relational-core/ast';
4
4
  import type {
5
5
  AnyBinaryBuilder,
6
- AnyExpressionSource,
6
+ AnyColumnBuilder,
7
7
  AnyOrderBuilder,
8
8
  NestedProjection,
9
9
  } from '@prisma-next/sql-relational-core/types';
10
10
  import type { ProjectionState } from '../utils/state';
11
11
 
12
- export type ProjectionInput = Record<string, AnyExpressionSource | boolean | NestedProjection>;
12
+ export type ProjectionInput = Record<string, AnyColumnBuilder | boolean | NestedProjection>;
13
13
 
14
14
  export interface MetaBuildArgs {
15
15
  readonly contract: SqlContract<SqlStorage>;
@@ -98,7 +98,7 @@ export function errorMissingParameter(paramName: string): never {
98
98
 
99
99
  export function errorInvalidProjectionValue(path: string[]): never {
100
100
  throw planInvalid(
101
- `Invalid projection value at path ${path.join('.')}: expected ExpressionSource (ColumnBuilder or ExpressionBuilder) or nested object`,
101
+ `Invalid projection value at path ${path.join('.')}: expected ColumnBuilder or nested object`,
102
102
  );
103
103
  }
104
104
 
@@ -1,14 +1,16 @@
1
1
  import type { TableRef } from '@prisma-next/sql-relational-core/ast';
2
2
  import type {
3
3
  AnyBinaryBuilder,
4
- AnyExpressionSource,
4
+ AnyColumnBuilder,
5
5
  AnyOrderBuilder,
6
6
  JoinOnPredicate,
7
7
  } from '@prisma-next/sql-relational-core/types';
8
8
 
9
9
  export interface ProjectionState {
10
10
  readonly aliases: string[];
11
- readonly columns: AnyExpressionSource[];
11
+ // columns can be null for include placeholders (when alias matches an include)
12
+ // This maintains array alignment but avoids creating invalid ColumnBuilders
13
+ readonly columns: (AnyColumnBuilder | null)[];
12
14
  }
13
15
 
14
16
  export interface JoinState {