@prisma-next/adapter-postgres 0.4.1 → 0.4.3

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 (59) hide show
  1. package/README.md +21 -15
  2. package/dist/adapter-_L4wXA4O.mjs +64 -0
  3. package/dist/adapter-_L4wXA4O.mjs.map +1 -0
  4. package/dist/adapter.d.mts +3 -8
  5. package/dist/adapter.d.mts.map +1 -1
  6. package/dist/adapter.mjs +1 -1
  7. package/dist/column-types.d.mts +15 -23
  8. package/dist/column-types.d.mts.map +1 -1
  9. package/dist/column-types.mjs +15 -58
  10. package/dist/column-types.mjs.map +1 -1
  11. package/dist/control.d.mts +73 -67
  12. package/dist/control.d.mts.map +1 -1
  13. package/dist/control.mjs +59 -162
  14. package/dist/control.mjs.map +1 -1
  15. package/dist/{descriptor-meta-BB9XPAFi.mjs → descriptor-meta-Dxnoq_rr.mjs} +27 -26
  16. package/dist/descriptor-meta-Dxnoq_rr.mjs.map +1 -0
  17. package/dist/operation-types.d.mts +11 -10
  18. package/dist/operation-types.d.mts.map +1 -1
  19. package/dist/runtime.d.mts +3 -11
  20. package/dist/runtime.d.mts.map +1 -1
  21. package/dist/runtime.mjs +23 -66
  22. package/dist/runtime.mjs.map +1 -1
  23. package/dist/{adapter-Du9Hr9Rl.mjs → sql-renderer-DLwYpnxz.mjs} +176 -113
  24. package/dist/sql-renderer-DLwYpnxz.mjs.map +1 -0
  25. package/dist/{types-TyL62f9Y.d.mts → types-tLtmYqCO.d.mts} +12 -1
  26. package/dist/types-tLtmYqCO.d.mts.map +1 -0
  27. package/dist/types.d.mts +1 -1
  28. package/package.json +22 -18
  29. package/src/core/adapter.ts +13 -626
  30. package/src/core/codec-lookup.ts +24 -0
  31. package/src/core/control-adapter.ts +88 -47
  32. package/src/core/descriptor-meta.ts +34 -16
  33. package/src/core/enum-control-hooks.ts +7 -2
  34. package/src/core/sql-renderer.ts +778 -0
  35. package/src/core/types.ts +11 -0
  36. package/src/exports/column-types.ts +14 -59
  37. package/src/exports/control.ts +11 -5
  38. package/src/exports/runtime.ts +29 -42
  39. package/src/types/operation-types.ts +19 -9
  40. package/dist/adapter-Du9Hr9Rl.mjs.map +0 -1
  41. package/dist/codec-ids-5g4Gwrgm.mjs +0 -29
  42. package/dist/codec-ids-5g4Gwrgm.mjs.map +0 -1
  43. package/dist/codec-types.d.mts +0 -107
  44. package/dist/codec-types.d.mts.map +0 -1
  45. package/dist/codec-types.mjs +0 -3
  46. package/dist/codecs-DiPlMi3-.mjs +0 -385
  47. package/dist/codecs-DiPlMi3-.mjs.map +0 -1
  48. package/dist/descriptor-meta-BB9XPAFi.mjs.map +0 -1
  49. package/dist/sql-utils-DkUJyZmA.mjs +0 -78
  50. package/dist/sql-utils-DkUJyZmA.mjs.map +0 -1
  51. package/dist/types-TyL62f9Y.d.mts.map +0 -1
  52. package/src/core/codec-ids.ts +0 -30
  53. package/src/core/codecs.ts +0 -645
  54. package/src/core/default-normalizer.ts +0 -145
  55. package/src/core/json-schema-type-expression.ts +0 -131
  56. package/src/core/json-schema-validator.ts +0 -53
  57. package/src/core/sql-utils.ts +0 -111
  58. package/src/core/standard-schema.ts +0 -71
  59. package/src/exports/codec-types.ts +0 -44
@@ -0,0 +1,778 @@
1
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
2
+ import {
3
+ type AggregateExpr,
4
+ type AnyExpression,
5
+ type AnyFromSource,
6
+ type AnyQueryAst,
7
+ type BinaryExpr,
8
+ type ColumnRef,
9
+ collectOrderedParamRefs,
10
+ type DeleteAst,
11
+ type InsertAst,
12
+ type InsertValue,
13
+ type JoinAst,
14
+ type JoinOnExpr,
15
+ type JsonArrayAggExpr,
16
+ type JsonObjectExpr,
17
+ type ListExpression,
18
+ LiteralExpr,
19
+ type NullCheckExpr,
20
+ type OperationExpr,
21
+ type OrderByItem,
22
+ type ParamRef,
23
+ type ProjectionItem,
24
+ type SelectAst,
25
+ type Codec as SqlCodec,
26
+ type SubqueryExpr,
27
+ type UpdateAst,
28
+ } from '@prisma-next/sql-relational-core/ast';
29
+ import { escapeLiteral, quoteIdentifier } from '@prisma-next/target-postgres/sql-utils';
30
+ import type { PostgresContract } from './types';
31
+
32
+ /**
33
+ * Postgres native types whose unknown-OID parameter inference is reliable in
34
+ * arbitrary expression positions. Parameters bound to a codec whose
35
+ * `meta.db.sql.postgres.nativeType` falls in this set are emitted as plain
36
+ * `$N`; everything else (including `json`, `jsonb`, extension types like
37
+ * `vector`, and unknown user types) is emitted as `$N::<nativeType>` so the
38
+ * planner picks an unambiguous overload.
39
+ *
40
+ * `json` / `jsonb` are intentionally excluded despite being Postgres builtins:
41
+ * their operator overloads make context inference unreliable in expression
42
+ * positions (e.g. `$1 -> 'key'` is ambiguous between the two).
43
+ *
44
+ * Spellings match the on-disk `meta.db.sql.postgres.nativeType` values in
45
+ * `@prisma-next/target-postgres`'s codec definitions, not the `udt_name`
46
+ * abbreviations that ADR 205 used as illustrative shorthand. The lookup-based
47
+ * cast policy compares against these strings directly.
48
+ */
49
+ const POSTGRES_INFERRABLE_NATIVE_TYPES: ReadonlySet<string> = new Set([
50
+ // Numeric
51
+ 'integer',
52
+ 'smallint',
53
+ 'bigint',
54
+ 'real',
55
+ 'double precision',
56
+ 'numeric',
57
+ // Boolean
58
+ 'boolean',
59
+ // Strings
60
+ 'text',
61
+ 'character',
62
+ 'character varying',
63
+ // Temporal
64
+ 'timestamp',
65
+ 'timestamp without time zone',
66
+ 'timestamp with time zone',
67
+ 'time',
68
+ 'timetz',
69
+ 'interval',
70
+ // Bit strings
71
+ 'bit',
72
+ 'bit varying',
73
+ ]);
74
+
75
+ function renderTypedParam(
76
+ index: number,
77
+ codecId: string | undefined,
78
+ codecLookup: CodecLookup,
79
+ ): string {
80
+ if (codecId === undefined) {
81
+ return `$${index}`;
82
+ }
83
+ // SQL codecs extend the framework `Codec` base with an optional
84
+ // `meta: CodecMeta`; the framework `CodecLookup.get` returns the base type,
85
+ // so we narrow to `SqlCodec` to read `meta`. Every codec actually
86
+ // registered into a SQL codec lookup conforms to `SqlCodec`.
87
+ const codec = codecLookup.get(codecId) as SqlCodec | undefined;
88
+ if (codec === undefined) {
89
+ throw new Error(
90
+ `Postgres lowering: ParamRef carries codecId "${codecId}" but the ` +
91
+ 'assembled codec lookup has no entry for it. This usually indicates ' +
92
+ 'a missing extension pack in the runtime stack — register the pack ' +
93
+ 'that contributes this codec (e.g. `extensionPacks: [pgvectorRuntime]`), ' +
94
+ 'or use the codec directly from `@prisma-next/target-postgres/codecs` ' +
95
+ "if it's a builtin.",
96
+ );
97
+ }
98
+ const nativeType = codec.meta?.db?.sql?.postgres?.nativeType;
99
+ if (nativeType !== undefined && !POSTGRES_INFERRABLE_NATIVE_TYPES.has(nativeType)) {
100
+ return `$${index}::${nativeType}`;
101
+ }
102
+ return `$${index}`;
103
+ }
104
+
105
+ /**
106
+ * Per-render carrier threaded through every helper. Bundles the param-index
107
+ * map (for `$N` numbering) and the assembled-stack `codecLookup` (for
108
+ * cast policy at the `renderTypedParam` chokepoint). Carrying both on a
109
+ * single value keeps helper signatures stable.
110
+ */
111
+ interface ParamIndexMap {
112
+ readonly indexMap: Map<ParamRef, number>;
113
+ readonly codecLookup: CodecLookup;
114
+ }
115
+
116
+ /**
117
+ * Render a SQL query AST to a Postgres-flavored `{ sql, params }` payload.
118
+ *
119
+ * Shared between the runtime (`PostgresAdapterImpl.lower`) and control
120
+ * (`PostgresControlAdapter.lower`) entrypoints so emit-time and run-time
121
+ * paths produce byte-identical output for the same AST.
122
+ */
123
+ export function renderLoweredSql(
124
+ ast: AnyQueryAst,
125
+ contract: PostgresContract,
126
+ codecLookup: CodecLookup,
127
+ ): { readonly sql: string; readonly params: readonly unknown[] } {
128
+ const orderedRefs = collectOrderedParamRefs(ast);
129
+ const indexMap = new Map<ParamRef, number>();
130
+ const params: unknown[] = orderedRefs.map((ref, i) => {
131
+ indexMap.set(ref, i + 1);
132
+ return ref.value;
133
+ });
134
+ const pim: ParamIndexMap = { indexMap, codecLookup };
135
+
136
+ const node = ast;
137
+ let sql: string;
138
+ switch (node.kind) {
139
+ case 'select':
140
+ sql = renderSelect(node, contract, pim);
141
+ break;
142
+ case 'insert':
143
+ sql = renderInsert(node, contract, pim);
144
+ break;
145
+ case 'update':
146
+ sql = renderUpdate(node, contract, pim);
147
+ break;
148
+ case 'delete':
149
+ sql = renderDelete(node, contract, pim);
150
+ break;
151
+ // v8 ignore next 4
152
+ default:
153
+ throw new Error(
154
+ `Unsupported AST node kind: ${(node satisfies never as { kind: string }).kind}`,
155
+ );
156
+ }
157
+
158
+ return Object.freeze({ sql, params: Object.freeze(params) });
159
+ }
160
+
161
+ function renderSelect(ast: SelectAst, contract: PostgresContract, pim: ParamIndexMap): string {
162
+ const selectClause = `SELECT ${renderDistinctPrefix(ast.distinct, ast.distinctOn, contract, pim)}${renderProjection(
163
+ ast.projection,
164
+ contract,
165
+ pim,
166
+ )}`;
167
+ const fromClause = `FROM ${renderSource(ast.from, contract, pim)}`;
168
+
169
+ const joinsClause = ast.joins?.length
170
+ ? ast.joins.map((join) => renderJoin(join, contract, pim)).join(' ')
171
+ : '';
172
+
173
+ const whereClause = ast.where ? `WHERE ${renderWhere(ast.where, contract, pim)}` : '';
174
+ const groupByClause = ast.groupBy?.length
175
+ ? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract, pim)).join(', ')}`
176
+ : '';
177
+ const havingClause = ast.having ? `HAVING ${renderWhere(ast.having, contract, pim)}` : '';
178
+ const orderClause = ast.orderBy?.length
179
+ ? `ORDER BY ${ast.orderBy
180
+ .map((order) => {
181
+ const expr = renderExpr(order.expr, contract, pim);
182
+ return `${expr} ${order.dir.toUpperCase()}`;
183
+ })
184
+ .join(', ')}`
185
+ : '';
186
+ const limitClause = typeof ast.limit === 'number' ? `LIMIT ${ast.limit}` : '';
187
+ const offsetClause = typeof ast.offset === 'number' ? `OFFSET ${ast.offset}` : '';
188
+
189
+ const clauses = [
190
+ selectClause,
191
+ fromClause,
192
+ joinsClause,
193
+ whereClause,
194
+ groupByClause,
195
+ havingClause,
196
+ orderClause,
197
+ limitClause,
198
+ offsetClause,
199
+ ]
200
+ .filter((part) => part.length > 0)
201
+ .join(' ');
202
+ return clauses.trim();
203
+ }
204
+
205
+ function renderProjection(
206
+ projection: ReadonlyArray<ProjectionItem>,
207
+ contract: PostgresContract,
208
+ pim: ParamIndexMap,
209
+ ): string {
210
+ return projection
211
+ .map((item) => {
212
+ const alias = quoteIdentifier(item.alias);
213
+ if (item.expr.kind === 'literal') {
214
+ return `${renderLiteral(item.expr)} AS ${alias}`;
215
+ }
216
+ return `${renderExpr(item.expr, contract, pim)} AS ${alias}`;
217
+ })
218
+ .join(', ');
219
+ }
220
+
221
+ function renderReturning(
222
+ items: ReadonlyArray<ProjectionItem>,
223
+ contract: PostgresContract,
224
+ pim: ParamIndexMap,
225
+ ): string {
226
+ return items
227
+ .map((item) => {
228
+ if (item.expr.kind === 'column-ref') {
229
+ const rendered = renderColumn(item.expr);
230
+ return item.expr.column === item.alias
231
+ ? rendered
232
+ : `${rendered} AS ${quoteIdentifier(item.alias)}`;
233
+ }
234
+ if (item.expr.kind === 'literal') {
235
+ return `${renderLiteral(item.expr)} AS ${quoteIdentifier(item.alias)}`;
236
+ }
237
+ return `${renderExpr(item.expr, contract, pim)} AS ${quoteIdentifier(item.alias)}`;
238
+ })
239
+ .join(', ');
240
+ }
241
+
242
+ function renderDistinctPrefix(
243
+ distinct: true | undefined,
244
+ distinctOn: ReadonlyArray<AnyExpression> | undefined,
245
+ contract: PostgresContract,
246
+ pim: ParamIndexMap,
247
+ ): string {
248
+ if (distinctOn && distinctOn.length > 0) {
249
+ const rendered = distinctOn.map((expr) => renderExpr(expr, contract, pim)).join(', ');
250
+ return `DISTINCT ON (${rendered}) `;
251
+ }
252
+ if (distinct) {
253
+ return 'DISTINCT ';
254
+ }
255
+ return '';
256
+ }
257
+
258
+ function renderSource(
259
+ source: AnyFromSource,
260
+ contract: PostgresContract,
261
+ pim: ParamIndexMap,
262
+ ): string {
263
+ const node = source;
264
+ switch (node.kind) {
265
+ case 'table-source': {
266
+ const table = quoteIdentifier(node.name);
267
+ if (!node.alias) {
268
+ return table;
269
+ }
270
+ return `${table} AS ${quoteIdentifier(node.alias)}`;
271
+ }
272
+ case 'derived-table-source':
273
+ return `(${renderSelect(node.query, contract, pim)}) AS ${quoteIdentifier(node.alias)}`;
274
+ // v8 ignore next 4
275
+ default:
276
+ throw new Error(
277
+ `Unsupported source node kind: ${(node satisfies never as { kind: string }).kind}`,
278
+ );
279
+ }
280
+ }
281
+
282
+ function assertScalarSubquery(query: SelectAst): void {
283
+ if (query.projection.length !== 1) {
284
+ throw new Error('Subquery expressions must project exactly one column');
285
+ }
286
+ }
287
+
288
+ function renderSubqueryExpr(
289
+ expr: SubqueryExpr,
290
+ contract: PostgresContract,
291
+ pim: ParamIndexMap,
292
+ ): string {
293
+ assertScalarSubquery(expr.query);
294
+ return `(${renderSelect(expr.query, contract, pim)})`;
295
+ }
296
+
297
+ function renderWhere(expr: AnyExpression, contract: PostgresContract, pim: ParamIndexMap): string {
298
+ return renderExpr(expr, contract, pim);
299
+ }
300
+
301
+ function renderNullCheck(
302
+ expr: NullCheckExpr,
303
+ contract: PostgresContract,
304
+ pim: ParamIndexMap,
305
+ ): string {
306
+ const rendered = renderExpr(expr.expr, contract, pim);
307
+ const renderedExpr = isAtomicExpressionKind(expr.expr.kind) ? rendered : `(${rendered})`;
308
+ return expr.isNull ? `${renderedExpr} IS NULL` : `${renderedExpr} IS NOT NULL`;
309
+ }
310
+
311
+ /**
312
+ * Atomic expression kinds whose rendered SQL is already self-delimited
313
+ * (a column reference, parameter, literal, function call, aggregate, etc.)
314
+ * and therefore does not need surrounding parentheses when used as the
315
+ * left operand of a postfix predicate like `IS NULL` or `IS NOT NULL`,
316
+ * or as either operand of a binary infix operator.
317
+ *
318
+ * Anything not in this set is treated as composite (binary, AND/OR/NOT,
319
+ * EXISTS, nested IS NULL, subqueries, operation templates) and gets
320
+ * wrapped to preserve grouping.
321
+ */
322
+ function isAtomicExpressionKind(kind: AnyExpression['kind']): boolean {
323
+ switch (kind) {
324
+ case 'column-ref':
325
+ case 'identifier-ref':
326
+ case 'param-ref':
327
+ case 'literal':
328
+ case 'aggregate':
329
+ case 'json-object':
330
+ case 'json-array-agg':
331
+ case 'list':
332
+ return true;
333
+ case 'subquery':
334
+ case 'operation':
335
+ case 'binary':
336
+ case 'and':
337
+ case 'or':
338
+ case 'exists':
339
+ case 'null-check':
340
+ case 'not':
341
+ return false;
342
+ }
343
+ }
344
+
345
+ function renderBinary(expr: BinaryExpr, contract: PostgresContract, pim: ParamIndexMap): string {
346
+ if (expr.right.kind === 'list' && expr.right.values.length === 0) {
347
+ if (expr.op === 'in') {
348
+ return 'FALSE';
349
+ }
350
+ if (expr.op === 'notIn') {
351
+ return 'TRUE';
352
+ }
353
+ }
354
+
355
+ const leftExpr = expr.left;
356
+ const left = renderExpr(leftExpr, contract, pim);
357
+ const leftRendered =
358
+ leftExpr.kind === 'operation' || leftExpr.kind === 'subquery' ? `(${left})` : left;
359
+
360
+ const rightNode = expr.right;
361
+ let right: string;
362
+ switch (rightNode.kind) {
363
+ case 'list':
364
+ right = renderListLiteral(rightNode, contract, pim);
365
+ break;
366
+ case 'literal':
367
+ right = renderLiteral(rightNode);
368
+ break;
369
+ case 'column-ref':
370
+ right = renderColumn(rightNode);
371
+ break;
372
+ case 'param-ref':
373
+ right = renderParamRef(rightNode, pim);
374
+ break;
375
+ default:
376
+ right = renderExpr(rightNode, contract, pim);
377
+ break;
378
+ }
379
+
380
+ const operatorMap: Record<BinaryExpr['op'], string> = {
381
+ eq: '=',
382
+ neq: '!=',
383
+ gt: '>',
384
+ lt: '<',
385
+ gte: '>=',
386
+ lte: '<=',
387
+ like: 'LIKE',
388
+ in: 'IN',
389
+ notIn: 'NOT IN',
390
+ };
391
+
392
+ return `${leftRendered} ${operatorMap[expr.op]} ${right}`;
393
+ }
394
+
395
+ function renderListLiteral(
396
+ expr: ListExpression,
397
+ contract: PostgresContract,
398
+ pim: ParamIndexMap,
399
+ ): string {
400
+ if (expr.values.length === 0) {
401
+ return '(NULL)';
402
+ }
403
+ const values = expr.values
404
+ .map((v) => {
405
+ if (v.kind === 'param-ref') return renderParamRef(v, pim);
406
+ if (v.kind === 'literal') return renderLiteral(v);
407
+ return renderExpr(v, contract, pim);
408
+ })
409
+ .join(', ');
410
+ return `(${values})`;
411
+ }
412
+
413
+ function renderColumn(ref: ColumnRef): string {
414
+ if (ref.table === 'excluded') {
415
+ return `excluded.${quoteIdentifier(ref.column)}`;
416
+ }
417
+ return `${quoteIdentifier(ref.table)}.${quoteIdentifier(ref.column)}`;
418
+ }
419
+
420
+ function renderAggregateExpr(
421
+ expr: AggregateExpr,
422
+ contract: PostgresContract,
423
+ pim: ParamIndexMap,
424
+ ): string {
425
+ const fn = expr.fn.toUpperCase();
426
+ if (!expr.expr) {
427
+ return `${fn}(*)`;
428
+ }
429
+ return `${fn}(${renderExpr(expr.expr, contract, pim)})`;
430
+ }
431
+
432
+ function renderJsonObjectExpr(
433
+ expr: JsonObjectExpr,
434
+ contract: PostgresContract,
435
+ pim: ParamIndexMap,
436
+ ): string {
437
+ const args = expr.entries
438
+ .flatMap((entry): [string, string] => {
439
+ const key = `'${escapeLiteral(entry.key)}'`;
440
+ if (entry.value.kind === 'literal') {
441
+ return [key, renderLiteral(entry.value)];
442
+ }
443
+ return [key, renderExpr(entry.value, contract, pim)];
444
+ })
445
+ .join(', ');
446
+ return `json_build_object(${args})`;
447
+ }
448
+
449
+ function renderOrderByItems(
450
+ items: ReadonlyArray<OrderByItem>,
451
+ contract: PostgresContract,
452
+ pim: ParamIndexMap,
453
+ ): string {
454
+ return items
455
+ .map((item) => `${renderExpr(item.expr, contract, pim)} ${item.dir.toUpperCase()}`)
456
+ .join(', ');
457
+ }
458
+
459
+ function renderJsonArrayAggExpr(
460
+ expr: JsonArrayAggExpr,
461
+ contract: PostgresContract,
462
+ pim: ParamIndexMap,
463
+ ): string {
464
+ const aggregateOrderBy =
465
+ expr.orderBy && expr.orderBy.length > 0
466
+ ? ` ORDER BY ${renderOrderByItems(expr.orderBy, contract, pim)}`
467
+ : '';
468
+ const aggregated = `json_agg(${renderExpr(expr.expr, contract, pim)}${aggregateOrderBy})`;
469
+ if (expr.onEmpty === 'emptyArray') {
470
+ return `coalesce(${aggregated}, json_build_array())`;
471
+ }
472
+ return aggregated;
473
+ }
474
+
475
+ function renderExpr(expr: AnyExpression, contract: PostgresContract, pim: ParamIndexMap): string {
476
+ const node = expr;
477
+ switch (node.kind) {
478
+ case 'column-ref':
479
+ return renderColumn(node);
480
+ case 'identifier-ref':
481
+ return quoteIdentifier(node.name);
482
+ case 'operation':
483
+ return renderOperation(node, contract, pim);
484
+ case 'subquery':
485
+ return renderSubqueryExpr(node, contract, pim);
486
+ case 'aggregate':
487
+ return renderAggregateExpr(node, contract, pim);
488
+ case 'json-object':
489
+ return renderJsonObjectExpr(node, contract, pim);
490
+ case 'json-array-agg':
491
+ return renderJsonArrayAggExpr(node, contract, pim);
492
+ case 'binary':
493
+ return renderBinary(node, contract, pim);
494
+ case 'and':
495
+ if (node.exprs.length === 0) {
496
+ return 'TRUE';
497
+ }
498
+ return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(' AND ')})`;
499
+ case 'or':
500
+ if (node.exprs.length === 0) {
501
+ return 'FALSE';
502
+ }
503
+ return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(' OR ')})`;
504
+ case 'exists': {
505
+ const notKeyword = node.notExists ? 'NOT ' : '';
506
+ const subquery = renderSelect(node.subquery, contract, pim);
507
+ return `${notKeyword}EXISTS (${subquery})`;
508
+ }
509
+ case 'null-check':
510
+ return renderNullCheck(node, contract, pim);
511
+ case 'not':
512
+ return `NOT (${renderExpr(node.expr, contract, pim)})`;
513
+ case 'param-ref':
514
+ return renderParamRef(node, pim);
515
+ case 'literal':
516
+ return renderLiteral(node);
517
+ case 'list':
518
+ return renderListLiteral(node, contract, pim);
519
+ // v8 ignore next 4
520
+ default:
521
+ throw new Error(
522
+ `Unsupported expression node kind: ${(node satisfies never as { kind: string }).kind}`,
523
+ );
524
+ }
525
+ }
526
+
527
+ function renderParamRef(ref: ParamRef, pim: ParamIndexMap): string {
528
+ const index = pim.indexMap.get(ref);
529
+ if (index === undefined) {
530
+ throw new Error('ParamRef not found in index map');
531
+ }
532
+ return renderTypedParam(index, ref.codecId, pim.codecLookup);
533
+ }
534
+
535
+ function renderLiteral(expr: LiteralExpr): string {
536
+ if (typeof expr.value === 'string') {
537
+ return `'${escapeLiteral(expr.value)}'`;
538
+ }
539
+ if (typeof expr.value === 'number' || typeof expr.value === 'boolean') {
540
+ return String(expr.value);
541
+ }
542
+ if (typeof expr.value === 'bigint') {
543
+ return String(expr.value);
544
+ }
545
+ if (expr.value === null) {
546
+ return 'NULL';
547
+ }
548
+ if (expr.value === undefined) {
549
+ return 'NULL';
550
+ }
551
+ if (expr.value instanceof Date) {
552
+ return `'${escapeLiteral(expr.value.toISOString())}'`;
553
+ }
554
+ if (Array.isArray(expr.value)) {
555
+ return `ARRAY[${expr.value.map((v: unknown) => renderLiteral(new LiteralExpr(v))).join(', ')}]`;
556
+ }
557
+ const json = JSON.stringify(expr.value);
558
+ if (json === undefined) {
559
+ return 'NULL';
560
+ }
561
+ return `'${escapeLiteral(json)}'`;
562
+ }
563
+
564
+ function renderOperation(
565
+ expr: OperationExpr,
566
+ contract: PostgresContract,
567
+ pim: ParamIndexMap,
568
+ ): string {
569
+ const self = renderExpr(expr.self, contract, pim);
570
+ const args = expr.args.map((arg) => {
571
+ return renderExpr(arg, contract, pim);
572
+ });
573
+
574
+ // Resolve `{{self}}` and `{{argN}}` from the original template in a single
575
+ // pass. Doing this with sequential `String.prototype.replace` calls is
576
+ // unsafe: a substituted fragment can itself contain text that matches a
577
+ // later token (e.g. an arg literal containing the substring `{{arg1}}`),
578
+ // and the next iteration would corrupt it. A single regex callback never
579
+ // re-scans already-substituted output.
580
+ return expr.lowering.template.replace(
581
+ /\{\{self\}\}|\{\{arg(\d+)\}\}/g,
582
+ (token, argIndex: string | undefined) => {
583
+ if (token === '{{self}}') {
584
+ return self;
585
+ }
586
+ const arg = args[Number(argIndex)];
587
+ if (arg === undefined) {
588
+ throw new Error(
589
+ `Operation lowering template for "${expr.method}" referenced missing argument {{arg${argIndex}}}; template has ${args.length} arg(s)`,
590
+ );
591
+ }
592
+ return arg;
593
+ },
594
+ );
595
+ }
596
+
597
+ function renderJoin(join: JoinAst, contract: PostgresContract, pim: ParamIndexMap): string {
598
+ const joinType = join.joinType.toUpperCase();
599
+ const lateral = join.lateral ? 'LATERAL ' : '';
600
+ const source = renderSource(join.source, contract, pim);
601
+ const onClause = renderJoinOn(join.on, contract, pim);
602
+ return `${joinType} JOIN ${lateral}${source} ON ${onClause}`;
603
+ }
604
+
605
+ function renderJoinOn(on: JoinOnExpr, contract: PostgresContract, pim: ParamIndexMap): string {
606
+ if (on.kind === 'eq-col-join-on') {
607
+ const left = renderColumn(on.left);
608
+ const right = renderColumn(on.right);
609
+ return `${left} = ${right}`;
610
+ }
611
+ return renderWhere(on, contract, pim);
612
+ }
613
+
614
+ function getInsertColumnOrder(
615
+ rows: ReadonlyArray<Record<string, InsertValue>>,
616
+ contract: PostgresContract,
617
+ tableName: string,
618
+ ): string[] {
619
+ const orderedColumns: string[] = [];
620
+ const seenColumns = new Set<string>();
621
+
622
+ for (const row of rows) {
623
+ for (const column of Object.keys(row)) {
624
+ if (seenColumns.has(column)) {
625
+ continue;
626
+ }
627
+ seenColumns.add(column);
628
+ orderedColumns.push(column);
629
+ }
630
+ }
631
+
632
+ if (orderedColumns.length > 0) {
633
+ return orderedColumns;
634
+ }
635
+
636
+ const table = contract.storage.tables[tableName];
637
+ if (!table) {
638
+ throw new Error(`INSERT target table not found in contract storage: ${tableName}`);
639
+ }
640
+ return Object.keys(table.columns);
641
+ }
642
+
643
+ function renderInsertValue(value: InsertValue | undefined, pim: ParamIndexMap): string {
644
+ if (!value || value.kind === 'default-value') {
645
+ return 'DEFAULT';
646
+ }
647
+
648
+ switch (value.kind) {
649
+ case 'param-ref':
650
+ return renderParamRef(value, pim);
651
+ case 'column-ref':
652
+ return renderColumn(value);
653
+ // v8 ignore next 4
654
+ default:
655
+ throw new Error(
656
+ `Unsupported value node in INSERT: ${(value satisfies never as { kind: string }).kind}`,
657
+ );
658
+ }
659
+ }
660
+
661
+ function renderInsert(ast: InsertAst, contract: PostgresContract, pim: ParamIndexMap): string {
662
+ const table = quoteIdentifier(ast.table.name);
663
+ const rows = ast.rows;
664
+ if (rows.length === 0) {
665
+ throw new Error('INSERT requires at least one row');
666
+ }
667
+ const hasExplicitValues = rows.some((row) => Object.keys(row).length > 0);
668
+ const insertClause = (() => {
669
+ if (!hasExplicitValues) {
670
+ if (rows.length === 1) {
671
+ return `INSERT INTO ${table} DEFAULT VALUES`;
672
+ }
673
+
674
+ const defaultColumns = getInsertColumnOrder(rows, contract, ast.table.name);
675
+ if (defaultColumns.length === 0) {
676
+ return `INSERT INTO ${table} VALUES ${rows.map(() => '()').join(', ')}`;
677
+ }
678
+
679
+ const quotedColumns = defaultColumns.map((column) => quoteIdentifier(column));
680
+ const defaultRow = `(${defaultColumns.map(() => 'DEFAULT').join(', ')})`;
681
+ return `INSERT INTO ${table} (${quotedColumns.join(', ')}) VALUES ${rows
682
+ .map(() => defaultRow)
683
+ .join(', ')}`;
684
+ }
685
+
686
+ const columnOrder = getInsertColumnOrder(rows, contract, ast.table.name);
687
+ const columns = columnOrder.map((column) => quoteIdentifier(column));
688
+ const values = rows
689
+ .map((row) => {
690
+ const renderedRow = columnOrder.map((column) => renderInsertValue(row[column], pim));
691
+ return `(${renderedRow.join(', ')})`;
692
+ })
693
+ .join(', ');
694
+
695
+ return `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${values}`;
696
+ })();
697
+ const onConflictClause = ast.onConflict
698
+ ? (() => {
699
+ const conflictColumns = ast.onConflict.columns.map((col) => quoteIdentifier(col.column));
700
+ if (conflictColumns.length === 0) {
701
+ throw new Error('INSERT onConflict requires at least one conflict column');
702
+ }
703
+
704
+ const action = ast.onConflict.action;
705
+ switch (action.kind) {
706
+ case 'do-nothing':
707
+ return ` ON CONFLICT (${conflictColumns.join(', ')}) DO NOTHING`;
708
+ case 'do-update-set': {
709
+ const updateEntries = Object.entries(action.set);
710
+ if (updateEntries.length === 0) {
711
+ throw new Error('INSERT onConflict do-update-set requires at least one assignment');
712
+ }
713
+ const updates = updateEntries.map(([colName, value]) => {
714
+ const target = quoteIdentifier(colName);
715
+ if (value.kind === 'param-ref') {
716
+ return `${target} = ${renderParamRef(value, pim)}`;
717
+ }
718
+ return `${target} = ${renderColumn(value)}`;
719
+ });
720
+ return ` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updates.join(', ')}`;
721
+ }
722
+ // v8 ignore next 4
723
+ default:
724
+ throw new Error(
725
+ `Unsupported onConflict action: ${(action satisfies never as { kind: string }).kind}`,
726
+ );
727
+ }
728
+ })()
729
+ : '';
730
+ const returningClause = ast.returning?.length
731
+ ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}`
732
+ : '';
733
+
734
+ return `${insertClause}${onConflictClause}${returningClause}`;
735
+ }
736
+
737
+ function renderUpdate(ast: UpdateAst, contract: PostgresContract, pim: ParamIndexMap): string {
738
+ const table = quoteIdentifier(ast.table.name);
739
+ const setEntries = Object.entries(ast.set);
740
+ if (setEntries.length === 0) {
741
+ throw new Error('UPDATE requires at least one SET assignment');
742
+ }
743
+ const setClauses = setEntries.map(([col, val]) => {
744
+ const column = quoteIdentifier(col);
745
+ let value: string;
746
+ switch (val.kind) {
747
+ case 'param-ref':
748
+ value = renderParamRef(val, pim);
749
+ break;
750
+ case 'column-ref':
751
+ value = renderColumn(val);
752
+ break;
753
+ // v8 ignore next 4
754
+ default:
755
+ throw new Error(
756
+ `Unsupported value node in UPDATE: ${(val satisfies never as { kind: string }).kind}`,
757
+ );
758
+ }
759
+ return `${column} = ${value}`;
760
+ });
761
+
762
+ const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : '';
763
+ const returningClause = ast.returning?.length
764
+ ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}`
765
+ : '';
766
+
767
+ return `UPDATE ${table} SET ${setClauses.join(', ')}${whereClause}${returningClause}`;
768
+ }
769
+
770
+ function renderDelete(ast: DeleteAst, contract: PostgresContract, pim: ParamIndexMap): string {
771
+ const table = quoteIdentifier(ast.table.name);
772
+ const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : '';
773
+ const returningClause = ast.returning?.length
774
+ ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}`
775
+ : '';
776
+
777
+ return `DELETE FROM ${table}${whereClause}${returningClause}`;
778
+ }