@prisma-next/adapter-postgres 0.3.0-dev.11 → 0.3.0-dev.114

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.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +64 -2
  3. package/dist/adapter-CWmWEFe1.mjs +361 -0
  4. package/dist/adapter-CWmWEFe1.mjs.map +1 -0
  5. package/dist/adapter.d.mts +23 -0
  6. package/dist/adapter.d.mts.map +1 -0
  7. package/dist/adapter.mjs +3 -0
  8. package/dist/codec-ids-Bsm9c7ns.mjs +29 -0
  9. package/dist/codec-ids-Bsm9c7ns.mjs.map +1 -0
  10. package/dist/codec-types.d.mts +141 -0
  11. package/dist/codec-types.d.mts.map +1 -0
  12. package/dist/codec-types.mjs +3 -0
  13. package/dist/codecs-DgJcyEBR.mjs +254 -0
  14. package/dist/codecs-DgJcyEBR.mjs.map +1 -0
  15. package/dist/column-types.d.mts +110 -0
  16. package/dist/column-types.d.mts.map +1 -0
  17. package/dist/column-types.mjs +180 -0
  18. package/dist/column-types.mjs.map +1 -0
  19. package/dist/control.d.mts +77 -0
  20. package/dist/control.d.mts.map +1 -0
  21. package/dist/control.mjs +773 -0
  22. package/dist/control.mjs.map +1 -0
  23. package/dist/descriptor-meta-l_dv8Nnn.mjs +884 -0
  24. package/dist/descriptor-meta-l_dv8Nnn.mjs.map +1 -0
  25. package/dist/runtime.d.mts +19 -0
  26. package/dist/runtime.d.mts.map +1 -0
  27. package/dist/runtime.mjs +99 -0
  28. package/dist/runtime.mjs.map +1 -0
  29. package/dist/sql-utils-CSfAGEwF.mjs +78 -0
  30. package/dist/sql-utils-CSfAGEwF.mjs.map +1 -0
  31. package/dist/types-aQLL6QVb.d.mts +19 -0
  32. package/dist/types-aQLL6QVb.d.mts.map +1 -0
  33. package/dist/types.d.mts +2 -0
  34. package/dist/types.mjs +1 -0
  35. package/package.json +39 -46
  36. package/src/core/adapter.ts +529 -256
  37. package/src/core/codec-ids.ts +28 -0
  38. package/src/core/codecs.ts +385 -36
  39. package/src/core/control-adapter.ts +404 -179
  40. package/src/core/control-mutation-defaults.ts +335 -0
  41. package/src/core/default-normalizer.ts +138 -0
  42. package/src/core/descriptor-meta.ts +296 -9
  43. package/src/core/enum-control-hooks.ts +733 -0
  44. package/src/core/json-schema-type-expression.ts +131 -0
  45. package/src/core/json-schema-validator.ts +53 -0
  46. package/src/core/sql-utils.ts +111 -0
  47. package/src/core/standard-schema.ts +71 -0
  48. package/src/core/types.ts +5 -3
  49. package/src/exports/codec-types.ts +73 -1
  50. package/src/exports/column-types.ts +233 -9
  51. package/src/exports/control.ts +20 -9
  52. package/src/exports/runtime.ts +76 -19
  53. package/dist/chunk-HD5YISNQ.js +0 -47
  54. package/dist/chunk-HD5YISNQ.js.map +0 -1
  55. package/dist/chunk-J3XSOAM2.js +0 -162
  56. package/dist/chunk-J3XSOAM2.js.map +0 -1
  57. package/dist/chunk-T6S3A6VT.js +0 -301
  58. package/dist/chunk-T6S3A6VT.js.map +0 -1
  59. package/dist/core/adapter.d.ts +0 -19
  60. package/dist/core/adapter.d.ts.map +0 -1
  61. package/dist/core/codecs.d.ts +0 -110
  62. package/dist/core/codecs.d.ts.map +0 -1
  63. package/dist/core/control-adapter.d.ts +0 -33
  64. package/dist/core/control-adapter.d.ts.map +0 -1
  65. package/dist/core/descriptor-meta.d.ts +0 -72
  66. package/dist/core/descriptor-meta.d.ts.map +0 -1
  67. package/dist/core/types.d.ts +0 -16
  68. package/dist/core/types.d.ts.map +0 -1
  69. package/dist/exports/adapter.d.ts +0 -2
  70. package/dist/exports/adapter.d.ts.map +0 -1
  71. package/dist/exports/adapter.js +0 -8
  72. package/dist/exports/adapter.js.map +0 -1
  73. package/dist/exports/codec-types.d.ts +0 -11
  74. package/dist/exports/codec-types.d.ts.map +0 -1
  75. package/dist/exports/codec-types.js +0 -7
  76. package/dist/exports/codec-types.js.map +0 -1
  77. package/dist/exports/column-types.d.ts +0 -17
  78. package/dist/exports/column-types.d.ts.map +0 -1
  79. package/dist/exports/column-types.js +0 -49
  80. package/dist/exports/column-types.js.map +0 -1
  81. package/dist/exports/control.d.ts +0 -8
  82. package/dist/exports/control.d.ts.map +0 -1
  83. package/dist/exports/control.js +0 -279
  84. package/dist/exports/control.js.map +0 -1
  85. package/dist/exports/runtime.d.ts +0 -15
  86. package/dist/exports/runtime.d.ts.map +0 -1
  87. package/dist/exports/runtime.js +0 -20
  88. package/dist/exports/runtime.js.map +0 -1
  89. package/dist/exports/types.d.ts +0 -2
  90. package/dist/exports/types.d.ts.map +0 -1
  91. package/dist/exports/types.js +0 -1
  92. package/dist/exports/types.js.map +0 -1
@@ -1,27 +1,61 @@
1
- import type {
2
- Adapter,
3
- AdapterProfile,
4
- BinaryExpr,
5
- ColumnRef,
6
- DeleteAst,
7
- ExistsExpr,
8
- IncludeRef,
9
- InsertAst,
10
- JoinAst,
1
+ import {
2
+ type Adapter,
3
+ type AdapterProfile,
4
+ type AggregateExpr,
5
+ type AnyExpression,
6
+ type AnyFromSource,
7
+ type AnyQueryAst,
8
+ type BinaryExpr,
9
+ type CodecParamsDescriptor,
10
+ type ColumnRef,
11
+ createCodecRegistry,
12
+ type DeleteAst,
13
+ type InsertAst,
14
+ type InsertValue,
15
+ type JoinAst,
16
+ type JoinOnExpr,
17
+ type JsonArrayAggExpr,
18
+ type JsonObjectExpr,
19
+ type ListExpression,
11
20
  LiteralExpr,
12
- LowererContext,
13
- OperationExpr,
14
- ParamRef,
15
- QueryAst,
16
- SelectAst,
17
- UpdateAst,
21
+ type LowererContext,
22
+ type NullCheckExpr,
23
+ type OperationExpr,
24
+ type OrderByItem,
25
+ type ParamRef,
26
+ type ProjectionItem,
27
+ type SelectAst,
28
+ type SubqueryExpr,
29
+ type UpdateAst,
18
30
  } from '@prisma-next/sql-relational-core/ast';
19
- import { createCodecRegistry, isOperationExpr } from '@prisma-next/sql-relational-core/ast';
31
+ import { ifDefined } from '@prisma-next/utils/defined';
32
+ import { PG_JSON_CODEC_ID, PG_JSONB_CODEC_ID } from './codec-ids';
20
33
  import { codecDefinitions } from './codecs';
34
+ import { escapeLiteral, quoteIdentifier } from './sql-utils';
21
35
  import type { PostgresAdapterOptions, PostgresContract, PostgresLoweredStatement } from './types';
22
36
 
23
37
  const VECTOR_CODEC_ID = 'pg/vector@1' as const;
24
38
 
39
+ function getCodecParamCast(codecId: string | undefined): string | undefined {
40
+ if (codecId === VECTOR_CODEC_ID) {
41
+ return 'vector';
42
+ }
43
+ if (codecId === PG_JSON_CODEC_ID) {
44
+ return 'json';
45
+ }
46
+ if (codecId === PG_JSONB_CODEC_ID) {
47
+ return 'jsonb';
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ function renderTypedParam(index: number, codecId: string | undefined): string {
53
+ const cast = getCodecParamCast(codecId);
54
+ return cast ? `$${index}::${cast}` : `$${index}`;
55
+ }
56
+
57
+ type ParamIndexMap = Map<ParamRef, number>;
58
+
25
59
  const defaultCapabilities = Object.freeze({
26
60
  postgres: {
27
61
  orderBy: true,
@@ -30,9 +64,30 @@ const defaultCapabilities = Object.freeze({
30
64
  jsonAgg: true,
31
65
  returning: true,
32
66
  },
67
+ sql: {
68
+ enums: true,
69
+ },
33
70
  });
34
71
 
35
- class PostgresAdapterImpl implements Adapter<QueryAst, PostgresContract, PostgresLoweredStatement> {
72
+ type AdapterCodec = (typeof codecDefinitions)[keyof typeof codecDefinitions]['codec'];
73
+ type ParameterizedCodec = AdapterCodec & {
74
+ readonly paramsSchema: NonNullable<AdapterCodec['paramsSchema']>;
75
+ };
76
+
77
+ const parameterizedCodecs: ReadonlyArray<CodecParamsDescriptor> = Object.values(codecDefinitions)
78
+ .map((definition) => definition.codec)
79
+ .filter((codec): codec is ParameterizedCodec => codec.paramsSchema !== undefined)
80
+ .map((codec) =>
81
+ Object.freeze({
82
+ codecId: codec.id,
83
+ paramsSchema: codec.paramsSchema,
84
+ ...ifDefined('init', codec.init),
85
+ }),
86
+ );
87
+
88
+ class PostgresAdapterImpl
89
+ implements Adapter<AnyQueryAst, PostgresContract, PostgresLoweredStatement>
90
+ {
36
91
  // These fields make the adapter instance structurally compatible with
37
92
  // RuntimeAdapterInstance<'sql', 'postgres'> without introducing a runtime-plane dependency.
38
93
  readonly familyId = 'sql' as const;
@@ -56,20 +111,43 @@ class PostgresAdapterImpl implements Adapter<QueryAst, PostgresContract, Postgre
56
111
  });
57
112
  }
58
113
 
59
- lower(ast: QueryAst, context: LowererContext<PostgresContract>) {
114
+ parameterizedCodecs(): ReadonlyArray<CodecParamsDescriptor> {
115
+ return parameterizedCodecs;
116
+ }
117
+
118
+ lower(ast: AnyQueryAst, context: LowererContext<PostgresContract>) {
119
+ const collectedParamRefs = ast.collectParamRefs();
120
+ const paramIndexMap: ParamIndexMap = new Map();
121
+ const params: unknown[] = [];
122
+ for (const ref of collectedParamRefs) {
123
+ if (paramIndexMap.has(ref)) {
124
+ continue;
125
+ }
126
+ paramIndexMap.set(ref, params.length + 1);
127
+ params.push(ref.value);
128
+ }
129
+
60
130
  let sql: string;
61
- const params = context.params ? [...context.params] : [];
62
-
63
- if (ast.kind === 'select') {
64
- sql = renderSelect(ast, context.contract);
65
- } else if (ast.kind === 'insert') {
66
- sql = renderInsert(ast, context.contract);
67
- } else if (ast.kind === 'update') {
68
- sql = renderUpdate(ast, context.contract);
69
- } else if (ast.kind === 'delete') {
70
- sql = renderDelete(ast, context.contract);
71
- } else {
72
- throw new Error(`Unsupported AST kind: ${(ast as { kind: string }).kind}`);
131
+
132
+ const node = ast;
133
+ switch (node.kind) {
134
+ case 'select':
135
+ sql = renderSelect(node, context.contract, paramIndexMap);
136
+ break;
137
+ case 'insert':
138
+ sql = renderInsert(node, context.contract, paramIndexMap);
139
+ break;
140
+ case 'update':
141
+ sql = renderUpdate(node, context.contract, paramIndexMap);
142
+ break;
143
+ case 'delete':
144
+ sql = renderDelete(node, context.contract, paramIndexMap);
145
+ break;
146
+ // v8 ignore next 4
147
+ default:
148
+ throw new Error(
149
+ `Unsupported AST node kind: ${(node satisfies never as { kind: string }).kind}`,
150
+ );
73
151
  }
74
152
 
75
153
  return Object.freeze({
@@ -79,84 +157,175 @@ class PostgresAdapterImpl implements Adapter<QueryAst, PostgresContract, Postgre
79
157
  }
80
158
  }
81
159
 
82
- function renderSelect(ast: SelectAst, contract?: PostgresContract): string {
83
- const selectClause = `SELECT ${renderProjection(ast, contract)}`;
84
- const fromClause = `FROM ${quoteIdentifier(ast.from.name)}`;
160
+ function renderSelect(ast: SelectAst, contract?: PostgresContract, pim?: ParamIndexMap): string {
161
+ const selectClause = `SELECT ${renderDistinctPrefix(ast.distinct, ast.distinctOn, contract, pim)}${renderProjection(
162
+ ast.projection,
163
+ contract,
164
+ pim,
165
+ )}`;
166
+ const fromClause = `FROM ${renderSource(ast.from, contract, pim)}`;
85
167
 
86
168
  const joinsClause = ast.joins?.length
87
- ? ast.joins.map((join) => renderJoin(join, contract)).join(' ')
88
- : '';
89
- const includesClause = ast.includes?.length
90
- ? ast.includes.map((include) => renderInclude(include, contract)).join(' ')
169
+ ? ast.joins.map((join) => renderJoin(join, contract, pim)).join(' ')
91
170
  : '';
92
171
 
93
- const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract)}` : '';
172
+ const whereClause = ast.where ? `WHERE ${renderWhere(ast.where, contract, pim)}` : '';
173
+ const groupByClause = ast.groupBy?.length
174
+ ? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract, pim)).join(', ')}`
175
+ : '';
176
+ const havingClause = ast.having ? `HAVING ${renderWhere(ast.having, contract, pim)}` : '';
94
177
  const orderClause = ast.orderBy?.length
95
- ? ` ORDER BY ${ast.orderBy
178
+ ? `ORDER BY ${ast.orderBy
96
179
  .map((order) => {
97
- const expr = renderExpr(order.expr as ColumnRef | OperationExpr, contract);
180
+ const expr = renderExpr(order.expr, contract, pim);
98
181
  return `${expr} ${order.dir.toUpperCase()}`;
99
182
  })
100
183
  .join(', ')}`
101
184
  : '';
102
- const limitClause = typeof ast.limit === 'number' ? ` LIMIT ${ast.limit}` : '';
103
-
104
- const clauses = [joinsClause, includesClause].filter(Boolean).join(' ');
105
- return `${selectClause} ${fromClause}${clauses ? ` ${clauses}` : ''}${whereClause}${orderClause}${limitClause}`.trim();
185
+ const limitClause = typeof ast.limit === 'number' ? `LIMIT ${ast.limit}` : '';
186
+ const offsetClause = typeof ast.offset === 'number' ? `OFFSET ${ast.offset}` : '';
187
+
188
+ const clauses = [
189
+ selectClause,
190
+ fromClause,
191
+ joinsClause,
192
+ whereClause,
193
+ groupByClause,
194
+ havingClause,
195
+ orderClause,
196
+ limitClause,
197
+ offsetClause,
198
+ ]
199
+ .filter((part) => part.length > 0)
200
+ .join(' ');
201
+ return clauses.trim();
106
202
  }
107
203
 
108
- function renderProjection(ast: SelectAst, contract?: PostgresContract): string {
109
- return ast.project
204
+ function renderProjection(
205
+ projection: ReadonlyArray<ProjectionItem>,
206
+ contract?: PostgresContract,
207
+ pim?: ParamIndexMap,
208
+ ): string {
209
+ return projection
110
210
  .map((item) => {
111
- const expr = item.expr as ColumnRef | IncludeRef | OperationExpr | LiteralExpr;
112
- if (expr.kind === 'includeRef') {
113
- // For include references, select the column from the LATERAL join alias
114
- // The LATERAL subquery returns a single column (the JSON array) with the alias
115
- // The table is aliased as {alias}_lateral, and the column inside is aliased as the include alias
116
- // We select it using table_alias.column_alias
117
- const tableAlias = `${expr.alias}_lateral`;
118
- return `${quoteIdentifier(tableAlias)}.${quoteIdentifier(expr.alias)} AS ${quoteIdentifier(item.alias)}`;
119
- }
120
- if (expr.kind === 'operation') {
121
- const operation = renderOperation(expr, contract);
122
- const alias = quoteIdentifier(item.alias);
123
- return `${operation} AS ${alias}`;
124
- }
125
- if (expr.kind === 'literal') {
126
- const literal = renderLiteral(expr);
127
- const alias = quoteIdentifier(item.alias);
128
- return `${literal} AS ${alias}`;
129
- }
130
- const column = renderColumn(expr as ColumnRef);
131
211
  const alias = quoteIdentifier(item.alias);
132
- return `${column} AS ${alias}`;
212
+ if (item.expr.kind === 'literal') {
213
+ return `${renderLiteral(item.expr)} AS ${alias}`;
214
+ }
215
+ return `${renderExpr(item.expr, contract, pim)} AS ${alias}`;
133
216
  })
134
217
  .join(', ');
135
218
  }
136
219
 
137
- function renderWhere(expr: BinaryExpr | ExistsExpr, contract?: PostgresContract): string {
138
- if (expr.kind === 'exists') {
139
- const notKeyword = expr.not ? 'NOT ' : '';
140
- const subquery = renderSelect(expr.subquery, contract);
141
- return `${notKeyword}EXISTS (${subquery})`;
220
+ function renderDistinctPrefix(
221
+ distinct: true | undefined,
222
+ distinctOn: ReadonlyArray<AnyExpression> | undefined,
223
+ contract?: PostgresContract,
224
+ pim?: ParamIndexMap,
225
+ ): string {
226
+ if (distinctOn && distinctOn.length > 0) {
227
+ const rendered = distinctOn.map((expr) => renderExpr(expr, contract, pim)).join(', ');
228
+ return `DISTINCT ON (${rendered}) `;
229
+ }
230
+ if (distinct) {
231
+ return 'DISTINCT ';
142
232
  }
143
- return renderBinary(expr, contract);
233
+ return '';
144
234
  }
145
235
 
146
- function renderBinary(expr: BinaryExpr, contract?: PostgresContract): string {
147
- const leftExpr = expr.left as ColumnRef | OperationExpr;
148
- const left = renderExpr(leftExpr, contract);
149
- // Handle both ParamRef and ColumnRef on the right side
150
- // (ColumnRef can appear in EXISTS subqueries for correlation)
151
- const rightExpr = expr.right as ParamRef | ColumnRef;
152
- const right =
153
- rightExpr.kind === 'col'
154
- ? renderColumn(rightExpr)
155
- : renderParam(rightExpr as ParamRef, contract);
156
- // Only wrap in parentheses if it's an operation expression
157
- const leftRendered = isOperationExpr(leftExpr) ? `(${left})` : left;
158
-
159
- // Map operators to SQL symbols
236
+ function renderSource(
237
+ source: AnyFromSource,
238
+ contract?: PostgresContract,
239
+ pim?: ParamIndexMap,
240
+ ): string {
241
+ const node = source;
242
+ switch (node.kind) {
243
+ case 'table-source': {
244
+ const table = quoteIdentifier(node.name);
245
+ if (!node.alias) {
246
+ return table;
247
+ }
248
+ return `${table} AS ${quoteIdentifier(node.alias)}`;
249
+ }
250
+ case 'derived-table-source':
251
+ return `(${renderSelect(node.query, contract, pim)}) AS ${quoteIdentifier(node.alias)}`;
252
+ // v8 ignore next 4
253
+ default:
254
+ throw new Error(
255
+ `Unsupported source node kind: ${(node satisfies never as { kind: string }).kind}`,
256
+ );
257
+ }
258
+ }
259
+
260
+ function assertScalarSubquery(query: SelectAst): void {
261
+ if (query.projection.length !== 1) {
262
+ throw new Error('Subquery expressions must project exactly one column');
263
+ }
264
+ }
265
+
266
+ function renderSubqueryExpr(
267
+ expr: SubqueryExpr,
268
+ contract?: PostgresContract,
269
+ pim?: ParamIndexMap,
270
+ ): string {
271
+ assertScalarSubquery(expr.query);
272
+ return `(${renderSelect(expr.query, contract, pim)})`;
273
+ }
274
+
275
+ function renderWhere(
276
+ expr: AnyExpression,
277
+ contract?: PostgresContract,
278
+ pim?: ParamIndexMap,
279
+ ): string {
280
+ return renderExpr(expr, contract, pim);
281
+ }
282
+
283
+ function renderNullCheck(
284
+ expr: NullCheckExpr,
285
+ contract?: PostgresContract,
286
+ pim?: ParamIndexMap,
287
+ ): string {
288
+ const rendered = renderExpr(expr.expr, contract, pim);
289
+ const renderedExpr =
290
+ expr.expr.kind === 'operation' || expr.expr.kind === 'subquery' ? `(${rendered})` : rendered;
291
+ return expr.isNull ? `${renderedExpr} IS NULL` : `${renderedExpr} IS NOT NULL`;
292
+ }
293
+
294
+ function renderBinary(expr: BinaryExpr, contract?: PostgresContract, pim?: ParamIndexMap): string {
295
+ if (expr.right.kind === 'list' && expr.right.values.length === 0) {
296
+ if (expr.op === 'in') {
297
+ return 'FALSE';
298
+ }
299
+ if (expr.op === 'notIn') {
300
+ return 'TRUE';
301
+ }
302
+ }
303
+
304
+ const leftExpr = expr.left;
305
+ const left = renderExpr(leftExpr, contract, pim);
306
+ const leftRendered =
307
+ leftExpr.kind === 'operation' || leftExpr.kind === 'subquery' ? `(${left})` : left;
308
+
309
+ const rightNode = expr.right;
310
+ let right: string;
311
+ switch (rightNode.kind) {
312
+ case 'list':
313
+ right = renderListLiteral(rightNode, pim);
314
+ break;
315
+ case 'literal':
316
+ right = renderLiteral(rightNode);
317
+ break;
318
+ case 'column-ref':
319
+ right = renderColumn(rightNode);
320
+ break;
321
+ case 'param-ref':
322
+ right = renderParamRef(rightNode, pim);
323
+ break;
324
+ default:
325
+ right = renderExpr(rightNode, contract, pim);
326
+ break;
327
+ }
328
+
160
329
  const operatorMap: Record<BinaryExpr['op'], string> = {
161
330
  eq: '=',
162
331
  neq: '!=',
@@ -164,249 +333,353 @@ function renderBinary(expr: BinaryExpr, contract?: PostgresContract): string {
164
333
  lt: '<',
165
334
  gte: '>=',
166
335
  lte: '<=',
336
+ like: 'LIKE',
337
+ ilike: 'ILIKE',
338
+ in: 'IN',
339
+ notIn: 'NOT IN',
167
340
  };
168
341
 
169
342
  return `${leftRendered} ${operatorMap[expr.op]} ${right}`;
170
343
  }
171
344
 
345
+ function renderListLiteral(expr: ListExpression, pim?: ParamIndexMap): string {
346
+ if (expr.values.length === 0) {
347
+ return '(NULL)';
348
+ }
349
+ const values = expr.values
350
+ .map((v) => {
351
+ if (v.kind === 'param-ref') return renderParamRef(v, pim);
352
+ if (v.kind === 'literal') return renderLiteral(v);
353
+ return renderExpr(v, undefined, pim);
354
+ })
355
+ .join(', ');
356
+ return `(${values})`;
357
+ }
358
+
172
359
  function renderColumn(ref: ColumnRef): string {
360
+ if (ref.table === 'excluded') {
361
+ return `excluded.${quoteIdentifier(ref.column)}`;
362
+ }
173
363
  return `${quoteIdentifier(ref.table)}.${quoteIdentifier(ref.column)}`;
174
364
  }
175
365
 
176
- function renderExpr(expr: ColumnRef | OperationExpr, contract?: PostgresContract): string {
177
- if (isOperationExpr(expr)) {
178
- return renderOperation(expr, contract);
366
+ function renderAggregateExpr(
367
+ expr: AggregateExpr,
368
+ contract?: PostgresContract,
369
+ pim?: ParamIndexMap,
370
+ ): string {
371
+ const fn = expr.fn.toUpperCase();
372
+ if (!expr.expr) {
373
+ return `${fn}(*)`;
179
374
  }
180
- return renderColumn(expr);
375
+ return `${fn}(${renderExpr(expr.expr, contract, pim)})`;
181
376
  }
182
377
 
183
- function renderParam(
184
- ref: ParamRef,
378
+ function renderJsonObjectExpr(
379
+ expr: JsonObjectExpr,
185
380
  contract?: PostgresContract,
186
- tableName?: string,
187
- columnName?: string,
381
+ pim?: ParamIndexMap,
188
382
  ): string {
189
- // Cast vector parameters to vector type for PostgreSQL
190
- if (contract && tableName && columnName) {
191
- const tableMeta = contract.storage.tables[tableName];
192
- const columnMeta = tableMeta?.columns[columnName];
193
- if (columnMeta?.codecId === VECTOR_CODEC_ID) {
194
- return `$${ref.index}::vector`;
383
+ const args = expr.entries
384
+ .flatMap((entry): [string, string] => {
385
+ const key = `'${escapeLiteral(entry.key)}'`;
386
+ if (entry.value.kind === 'literal') {
387
+ return [key, renderLiteral(entry.value)];
388
+ }
389
+ return [key, renderExpr(entry.value, contract, pim)];
390
+ })
391
+ .join(', ');
392
+ return `json_build_object(${args})`;
393
+ }
394
+
395
+ function renderOrderByItems(
396
+ items: ReadonlyArray<OrderByItem>,
397
+ contract?: PostgresContract,
398
+ pim?: ParamIndexMap,
399
+ ): string {
400
+ return items
401
+ .map((item) => `${renderExpr(item.expr, contract, pim)} ${item.dir.toUpperCase()}`)
402
+ .join(', ');
403
+ }
404
+
405
+ function renderJsonArrayAggExpr(
406
+ expr: JsonArrayAggExpr,
407
+ contract?: PostgresContract,
408
+ pim?: ParamIndexMap,
409
+ ): string {
410
+ const aggregateOrderBy =
411
+ expr.orderBy && expr.orderBy.length > 0
412
+ ? ` ORDER BY ${renderOrderByItems(expr.orderBy, contract, pim)}`
413
+ : '';
414
+ const aggregated = `json_agg(${renderExpr(expr.expr, contract, pim)}${aggregateOrderBy})`;
415
+ if (expr.onEmpty === 'emptyArray') {
416
+ return `coalesce(${aggregated}, json_build_array())`;
417
+ }
418
+ return aggregated;
419
+ }
420
+
421
+ function renderExpr(expr: AnyExpression, contract?: PostgresContract, pim?: ParamIndexMap): string {
422
+ const node = expr;
423
+ switch (node.kind) {
424
+ case 'column-ref':
425
+ return renderColumn(node);
426
+ case 'identifier-ref':
427
+ return quoteIdentifier(node.name);
428
+ case 'operation':
429
+ return renderOperation(node, contract, pim);
430
+ case 'subquery':
431
+ return renderSubqueryExpr(node, contract, pim);
432
+ case 'aggregate':
433
+ return renderAggregateExpr(node, contract, pim);
434
+ case 'json-object':
435
+ return renderJsonObjectExpr(node, contract, pim);
436
+ case 'json-array-agg':
437
+ return renderJsonArrayAggExpr(node, contract, pim);
438
+ case 'binary':
439
+ return renderBinary(node, contract, pim);
440
+ case 'and':
441
+ if (node.exprs.length === 0) {
442
+ return 'TRUE';
443
+ }
444
+ return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(' AND ')})`;
445
+ case 'or':
446
+ if (node.exprs.length === 0) {
447
+ return 'FALSE';
448
+ }
449
+ return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(' OR ')})`;
450
+ case 'exists': {
451
+ const notKeyword = node.notExists ? 'NOT ' : '';
452
+ const subquery = renderSelect(node.subquery, contract, pim);
453
+ return `${notKeyword}EXISTS (${subquery})`;
195
454
  }
455
+ case 'null-check':
456
+ return renderNullCheck(node, contract, pim);
457
+ case 'not':
458
+ return `NOT (${renderExpr(node.expr, contract, pim)})`;
459
+ case 'param-ref':
460
+ return renderParamRef(node, pim);
461
+ case 'literal':
462
+ return renderLiteral(node);
463
+ case 'list':
464
+ return renderListLiteral(node, pim);
465
+ // v8 ignore next 4
466
+ default:
467
+ throw new Error(
468
+ `Unsupported expression node kind: ${(node satisfies never as { kind: string }).kind}`,
469
+ );
196
470
  }
197
- return `$${ref.index}`;
471
+ }
472
+
473
+ function renderParamRef(ref: ParamRef, pim?: ParamIndexMap): string {
474
+ const index = pim?.get(ref);
475
+ if (index === undefined) {
476
+ throw new Error('ParamRef not found in index map');
477
+ }
478
+ return renderTypedParam(index, ref.codecId);
198
479
  }
199
480
 
200
481
  function renderLiteral(expr: LiteralExpr): string {
201
482
  if (typeof expr.value === 'string') {
202
- return `'${expr.value.replace(/'/g, "''")}'`;
483
+ return `'${escapeLiteral(expr.value)}'`;
203
484
  }
204
485
  if (typeof expr.value === 'number' || typeof expr.value === 'boolean') {
205
486
  return String(expr.value);
206
487
  }
488
+ if (typeof expr.value === 'bigint') {
489
+ return String(expr.value);
490
+ }
207
491
  if (expr.value === null) {
208
492
  return 'NULL';
209
493
  }
494
+ if (expr.value === undefined) {
495
+ return 'NULL';
496
+ }
497
+ if (expr.value instanceof Date) {
498
+ return `'${escapeLiteral(expr.value.toISOString())}'`;
499
+ }
210
500
  if (Array.isArray(expr.value)) {
211
- return `ARRAY[${expr.value.map((v: unknown) => renderLiteral({ kind: 'literal', value: v })).join(', ')}]`;
501
+ return `ARRAY[${expr.value.map((v: unknown) => renderLiteral(new LiteralExpr(v))).join(', ')}]`;
212
502
  }
213
- return JSON.stringify(expr.value);
503
+ const json = JSON.stringify(expr.value);
504
+ if (json === undefined) {
505
+ return 'NULL';
506
+ }
507
+ return `'${escapeLiteral(json)}'`;
214
508
  }
215
509
 
216
- function renderOperation(expr: OperationExpr, contract?: PostgresContract): string {
217
- const self = renderExpr(expr.self, contract);
218
- // For vector operations, cast param arguments to vector type
219
- const isVectorOperation = expr.forTypeId === VECTOR_CODEC_ID;
220
- const args = expr.args.map((arg: ColumnRef | ParamRef | LiteralExpr | OperationExpr) => {
221
- if (arg.kind === 'col') {
222
- return renderColumn(arg);
223
- }
224
- if (arg.kind === 'param') {
225
- // Cast vector operation parameters to vector type
226
- return isVectorOperation ? `$${arg.index}::vector` : renderParam(arg, contract);
227
- }
228
- if (arg.kind === 'literal') {
229
- return renderLiteral(arg);
230
- }
231
- if (arg.kind === 'operation') {
232
- return renderOperation(arg, contract);
233
- }
234
- const _exhaustive: never = arg;
235
- throw new Error(`Unsupported argument kind: ${(_exhaustive as { kind: string }).kind}`);
510
+ function renderOperation(
511
+ expr: OperationExpr,
512
+ contract?: PostgresContract,
513
+ pim?: ParamIndexMap,
514
+ ): string {
515
+ const self = renderExpr(expr.self, contract, pim);
516
+ const args = expr.args.map((arg) => {
517
+ return renderExpr(arg, contract, pim);
236
518
  });
237
519
 
238
520
  let result = expr.lowering.template;
239
- result = result.replace(/\$\{self\}/g, self);
521
+ result = result.replace(/\{\{self\}\}/g, self);
240
522
  for (let i = 0; i < args.length; i++) {
241
- result = result.replace(new RegExp(`\\$\\{arg${i}\\}`, 'g'), args[i] ?? '');
242
- }
243
-
244
- if (expr.lowering.strategy === 'function') {
245
- return result;
523
+ result = result.replace(new RegExp(`\\{\\{arg${i}\\}\\}`, 'g'), args[i] ?? '');
246
524
  }
247
525
 
248
526
  return result;
249
527
  }
250
528
 
251
- function renderJoin(join: JoinAst, _contract?: PostgresContract): string {
529
+ function renderJoin(join: JoinAst, contract?: PostgresContract, pim?: ParamIndexMap): string {
252
530
  const joinType = join.joinType.toUpperCase();
253
- const table = quoteIdentifier(join.table.name);
254
- const onClause = renderJoinOn(join.on);
255
- return `${joinType} JOIN ${table} ON ${onClause}`;
531
+ const lateral = join.lateral ? 'LATERAL ' : '';
532
+ const source = renderSource(join.source, contract, pim);
533
+ const onClause = renderJoinOn(join.on, contract, pim);
534
+ return `${joinType} JOIN ${lateral}${source} ON ${onClause}`;
256
535
  }
257
536
 
258
- function renderJoinOn(on: JoinAst['on']): string {
259
- if (on.kind === 'eqCol') {
537
+ function renderJoinOn(on: JoinOnExpr, contract?: PostgresContract, pim?: ParamIndexMap): string {
538
+ if (on.kind === 'eq-col-join-on') {
260
539
  const left = renderColumn(on.left);
261
540
  const right = renderColumn(on.right);
262
541
  return `${left} = ${right}`;
263
542
  }
264
- throw new Error(`Unsupported join ON expression kind: ${on.kind}`);
543
+ return renderWhere(on, contract, pim);
265
544
  }
266
545
 
267
- function renderInclude(
268
- include: NonNullable<SelectAst['includes']>[number],
269
- contract?: PostgresContract,
270
- ): string {
271
- const alias = include.alias;
546
+ function getInsertColumnOrder(
547
+ rows: ReadonlyArray<Record<string, InsertValue>>,
548
+ contract: PostgresContract,
549
+ tableName: string,
550
+ ): string[] {
551
+ const orderedColumns: string[] = [];
552
+ const seenColumns = new Set<string>();
553
+
554
+ for (const row of rows) {
555
+ for (const column of Object.keys(row)) {
556
+ if (seenColumns.has(column)) {
557
+ continue;
558
+ }
559
+ seenColumns.add(column);
560
+ orderedColumns.push(column);
561
+ }
562
+ }
272
563
 
273
- // Build the lateral subquery
274
- const childProjection = include.child.project
275
- .map((item: { alias: string; expr: ColumnRef | OperationExpr }) => {
276
- const expr = renderExpr(item.expr, contract);
277
- return `'${item.alias}', ${expr}`;
278
- })
279
- .join(', ');
564
+ if (orderedColumns.length > 0) {
565
+ return orderedColumns;
566
+ }
280
567
 
281
- const jsonBuildObject = `json_build_object(${childProjection})`;
568
+ return Object.keys(contract.storage.tables[tableName]?.columns ?? {});
569
+ }
282
570
 
283
- // Build the ON condition from the include's ON clause - this goes in the WHERE clause
284
- const onCondition = renderJoinOn(include.child.on);
571
+ function renderInsertValue(value: InsertValue | undefined, pim?: ParamIndexMap): string {
572
+ if (!value || value.kind === 'default-value') {
573
+ return 'DEFAULT';
574
+ }
285
575
 
286
- // Build WHERE clause: combine ON condition with any additional WHERE clauses
287
- let whereClause = ` WHERE ${onCondition}`;
288
- if (include.child.where) {
289
- whereClause += ` AND ${renderWhere(include.child.where, contract)}`;
576
+ switch (value.kind) {
577
+ case 'param-ref':
578
+ return renderParamRef(value, pim);
579
+ case 'column-ref':
580
+ return renderColumn(value);
581
+ // v8 ignore next 4
582
+ default:
583
+ throw new Error(
584
+ `Unsupported value node in INSERT: ${(value satisfies never as { kind: string }).kind}`,
585
+ );
290
586
  }
587
+ }
291
588
 
292
- // Add ORDER BY if present - it goes inside json_agg() call
293
- const childOrderBy = include.child.orderBy?.length
294
- ? ` ORDER BY ${include.child.orderBy
295
- .map(
296
- (order: { expr: ColumnRef | OperationExpr; dir: string }) =>
297
- `${renderExpr(order.expr, contract)} ${order.dir.toUpperCase()}`,
298
- )
299
- .join(', ')}`
300
- : '';
589
+ function renderInsert(ast: InsertAst, contract: PostgresContract, pim?: ParamIndexMap): string {
590
+ const table = quoteIdentifier(ast.table.name);
591
+ const rows = ast.rows;
592
+ if (rows.length === 0) {
593
+ throw new Error('INSERT requires at least one row');
594
+ }
595
+ const hasExplicitValues = rows.some((row) => Object.keys(row).length > 0);
596
+ const insertClause = (() => {
597
+ if (!hasExplicitValues) {
598
+ if (rows.length === 1) {
599
+ return `INSERT INTO ${table} DEFAULT VALUES`;
600
+ }
301
601
 
302
- // Add LIMIT if present
303
- const childLimit = typeof include.child.limit === 'number' ? ` LIMIT ${include.child.limit}` : '';
304
-
305
- // Build the lateral subquery
306
- // When ORDER BY is present without LIMIT, it goes inside json_agg() call: json_agg(expr ORDER BY ...)
307
- // When LIMIT is present (with or without ORDER BY), we need to wrap in a subquery
308
- const childTable = quoteIdentifier(include.child.table.name);
309
- let subquery: string;
310
- if (typeof include.child.limit === 'number') {
311
- // With LIMIT, we need to wrap in a subquery
312
- // Select individual columns in inner query, then aggregate
313
- // Create a map of column references to their aliases for ORDER BY
314
- // Only ColumnRef can be mapped (OperationExpr doesn't have table/column properties)
315
- const columnAliasMap = new Map<string, string>();
316
- for (const item of include.child.project) {
317
- if (item.expr.kind === 'col') {
318
- const columnKey = `${item.expr.table}.${item.expr.column}`;
319
- columnAliasMap.set(columnKey, item.alias);
602
+ const defaultColumns = getInsertColumnOrder(rows, contract, ast.table.name);
603
+ if (defaultColumns.length === 0) {
604
+ return `INSERT INTO ${table} VALUES ${rows.map(() => '()').join(', ')}`;
320
605
  }
606
+
607
+ const quotedColumns = defaultColumns.map((column) => quoteIdentifier(column));
608
+ const defaultRow = `(${defaultColumns.map(() => 'DEFAULT').join(', ')})`;
609
+ return `INSERT INTO ${table} (${quotedColumns.join(', ')}) VALUES ${rows
610
+ .map(() => defaultRow)
611
+ .join(', ')}`;
321
612
  }
322
613
 
323
- const innerColumns = include.child.project
324
- .map((item: { alias: string; expr: ColumnRef | OperationExpr }) => {
325
- const expr = renderExpr(item.expr, contract);
326
- return `${expr} AS ${quoteIdentifier(item.alias)}`;
614
+ const columnOrder = getInsertColumnOrder(rows, contract, ast.table.name);
615
+ const columns = columnOrder.map((column) => quoteIdentifier(column));
616
+ const values = rows
617
+ .map((row) => {
618
+ const renderedRow = columnOrder.map((column) => renderInsertValue(row[column], pim));
619
+ return `(${renderedRow.join(', ')})`;
327
620
  })
328
621
  .join(', ');
329
622
 
330
- // For ORDER BY, use column aliases if the column is in the SELECT list
331
- const childOrderByWithAliases = include.child.orderBy?.length
332
- ? ` ORDER BY ${include.child.orderBy
333
- .map((order: { expr: ColumnRef | OperationExpr; dir: string }) => {
334
- if (order.expr.kind === 'col') {
335
- const columnKey = `${order.expr.table}.${order.expr.column}`;
336
- const alias = columnAliasMap.get(columnKey);
337
- if (alias) {
338
- return `${quoteIdentifier(alias)} ${order.dir.toUpperCase()}`;
623
+ return `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${values}`;
624
+ })();
625
+ const onConflictClause = ast.onConflict
626
+ ? (() => {
627
+ const conflictColumns = ast.onConflict.columns.map((col) => quoteIdentifier(col.column));
628
+ if (conflictColumns.length === 0) {
629
+ throw new Error('INSERT onConflict requires at least one conflict column');
630
+ }
631
+
632
+ const action = ast.onConflict.action;
633
+ switch (action.kind) {
634
+ case 'do-nothing':
635
+ return ` ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
636
+ case 'do-update-set': {
637
+ const updates = Object.entries(action.set).map(([colName, value]) => {
638
+ const target = quoteIdentifier(colName);
639
+ if (value.kind === 'param-ref') {
640
+ return `${target} = ${renderParamRef(value, pim)}`;
339
641
  }
340
- }
341
- return `${renderExpr(order.expr, contract)} ${order.dir.toUpperCase()}`;
342
- })
343
- .join(', ')}`
344
- : '';
345
-
346
- const innerSelect = `SELECT ${innerColumns} FROM ${childTable}${whereClause}${childOrderByWithAliases}${childLimit}`;
347
- subquery = `(SELECT json_agg(row_to_json(sub.*)) AS ${quoteIdentifier(alias)} FROM (${innerSelect}) sub)`;
348
- } else if (childOrderBy) {
349
- // With ORDER BY but no LIMIT, ORDER BY goes inside json_agg()
350
- subquery = `(SELECT json_agg(${jsonBuildObject}${childOrderBy}) AS ${quoteIdentifier(alias)} FROM ${childTable}${whereClause})`;
351
- } else {
352
- // No ORDER BY or LIMIT
353
- subquery = `(SELECT json_agg(${jsonBuildObject}) AS ${quoteIdentifier(alias)} FROM ${childTable}${whereClause})`;
354
- }
355
-
356
- // Return the LATERAL join with ON true (the condition is in the WHERE clause)
357
- // The subquery returns a single column (the JSON array) with the alias
358
- // We use a different alias for the table to avoid ambiguity when selecting the column
359
- const tableAlias = `${alias}_lateral`;
360
- return `LEFT JOIN LATERAL ${subquery} AS ${quoteIdentifier(tableAlias)} ON true`;
361
- }
362
-
363
- function quoteIdentifier(identifier: string): string {
364
- return `"${identifier.replace(/"/g, '""')}"`;
365
- }
366
-
367
- function renderInsert(ast: InsertAst, contract: PostgresContract): string {
368
- const table = quoteIdentifier(ast.table.name);
369
- const columns = Object.keys(ast.values).map((col) => quoteIdentifier(col));
370
- const tableMeta = contract.storage.tables[ast.table.name];
371
- const values = Object.entries(ast.values).map(([colName, val]) => {
372
- if (val.kind === 'param') {
373
- const columnMeta = tableMeta?.columns[colName];
374
- const isVector = columnMeta?.codecId === VECTOR_CODEC_ID;
375
- return isVector ? `$${val.index}::vector` : `$${val.index}`;
376
- }
377
- if (val.kind === 'col') {
378
- return `${quoteIdentifier(val.table)}.${quoteIdentifier(val.column)}`;
379
- }
380
- throw new Error(`Unsupported value kind in INSERT: ${(val as { kind: string }).kind}`);
381
- });
382
-
383
- const insertClause = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${values.join(', ')})`;
642
+ return `${target} = ${renderColumn(value)}`;
643
+ });
644
+ return ` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updates.join(', ')}`;
645
+ }
646
+ // v8 ignore next 4
647
+ default:
648
+ throw new Error(
649
+ `Unsupported onConflict action: ${(action satisfies never as { kind: string }).kind}`,
650
+ );
651
+ }
652
+ })()
653
+ : '';
384
654
  const returningClause = ast.returning?.length
385
655
  ? ` RETURNING ${ast.returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`
386
656
  : '';
387
657
 
388
- return `${insertClause}${returningClause}`;
658
+ return `${insertClause}${onConflictClause}${returningClause}`;
389
659
  }
390
660
 
391
- function renderUpdate(ast: UpdateAst, contract: PostgresContract): string {
661
+ function renderUpdate(ast: UpdateAst, contract: PostgresContract, pim?: ParamIndexMap): string {
392
662
  const table = quoteIdentifier(ast.table.name);
393
- const tableMeta = contract.storage.tables[ast.table.name];
394
663
  const setClauses = Object.entries(ast.set).map(([col, val]) => {
395
664
  const column = quoteIdentifier(col);
396
665
  let value: string;
397
- if (val.kind === 'param') {
398
- const columnMeta = tableMeta?.columns[col];
399
- const isVector = columnMeta?.codecId === VECTOR_CODEC_ID;
400
- value = isVector ? `$${val.index}::vector` : `$${val.index}`;
401
- } else if (val.kind === 'col') {
402
- value = `${quoteIdentifier(val.table)}.${quoteIdentifier(val.column)}`;
403
- } else {
404
- throw new Error(`Unsupported value kind in UPDATE: ${(val as { kind: string }).kind}`);
666
+ switch (val.kind) {
667
+ case 'param-ref':
668
+ value = renderParamRef(val, pim);
669
+ break;
670
+ case 'column-ref':
671
+ value = renderColumn(val);
672
+ break;
673
+ // v8 ignore next 4
674
+ default:
675
+ throw new Error(
676
+ `Unsupported value node in UPDATE: ${(val satisfies never as { kind: string }).kind}`,
677
+ );
405
678
  }
406
679
  return `${column} = ${value}`;
407
680
  });
408
681
 
409
- const whereClause = ` WHERE ${renderBinary(ast.where, contract)}`;
682
+ const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : '';
410
683
  const returningClause = ast.returning?.length
411
684
  ? ` RETURNING ${ast.returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`
412
685
  : '';
@@ -414,9 +687,9 @@ function renderUpdate(ast: UpdateAst, contract: PostgresContract): string {
414
687
  return `UPDATE ${table} SET ${setClauses.join(', ')}${whereClause}${returningClause}`;
415
688
  }
416
689
 
417
- function renderDelete(ast: DeleteAst, contract?: PostgresContract): string {
690
+ function renderDelete(ast: DeleteAst, contract?: PostgresContract, pim?: ParamIndexMap): string {
418
691
  const table = quoteIdentifier(ast.table.name);
419
- const whereClause = ` WHERE ${renderBinary(ast.where, contract)}`;
692
+ const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : '';
420
693
  const returningClause = ast.returning?.length
421
694
  ? ` RETURNING ${ast.returning.map((col) => `${quoteIdentifier(col.table)}.${quoteIdentifier(col.column)}`).join(', ')}`
422
695
  : '';