@prisma-next/adapter-postgres 0.3.0-pr.99.5 → 0.3.0

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