@dbsp/adapter-pgsql 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -94,1854 +94,2309 @@ var init_naming_plugin = __esm({
94
94
  }
95
95
  });
96
96
 
97
- // src/ast-helpers.ts
98
- import { normalizeSQL } from "@dbsp/core";
99
- function applyReturningList(stmt, returning) {
100
- if (returning && returning.length > 0) {
101
- stmt.returningList = returning;
97
+ // src/validate.ts
98
+ function validateIdentifier(value, type) {
99
+ if (!value || value.length === 0) {
100
+ throw new InvalidIdentifierError(value, type, "cannot be empty");
102
101
  }
103
- }
104
- function stringNode(value) {
105
- return { String: { sval: value } };
106
- }
107
- function integerNode(value) {
108
- return { Integer: { ival: value } };
109
- }
110
- function floatNode(value) {
111
- return { Float: { fval: value } };
112
- }
113
- function booleanConstNode(value) {
114
- return {
115
- A_Const: {
116
- boolval: { boolval: value }
102
+ if (value.length > 63) {
103
+ throw new InvalidIdentifierError(
104
+ value,
105
+ type,
106
+ `exceeds maximum length of 63 characters (got ${value.length})`
107
+ );
108
+ }
109
+ const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
110
+ if (!validIdentifierPattern.test(value)) {
111
+ if (/[\x00-\x1f\x7f]/.test(value)) {
112
+ throw new InvalidIdentifierError(
113
+ value,
114
+ type,
115
+ "contains control characters"
116
+ );
117
117
  }
118
- };
119
- }
120
- function nullConstNode() {
121
- return {
122
- A_Const: {
123
- isnull: true
118
+ if (/^[0-9]/.test(value)) {
119
+ throw new InvalidIdentifierError(
120
+ value,
121
+ type,
122
+ "cannot start with a digit"
123
+ );
124
124
  }
125
- };
126
- }
127
- function columnRef(column, table, schema, naming = identityNaming) {
128
- const fields = [];
129
- if (schema) {
130
- fields.push(stringNode(naming.toDatabase(schema)));
125
+ if (/[^\w$]/.test(value)) {
126
+ throw new InvalidIdentifierError(
127
+ value,
128
+ type,
129
+ "contains invalid characters (only letters, digits, underscore, and $ allowed)"
130
+ );
131
+ }
132
+ throw new InvalidIdentifierError(
133
+ value,
134
+ type,
135
+ "does not match valid identifier pattern"
136
+ );
131
137
  }
132
- if (table) {
133
- fields.push(stringNode(naming.toDatabase(table)));
138
+ if (value.includes("\0")) {
139
+ throw new InvalidIdentifierError(value, type, "contains null byte");
134
140
  }
135
- fields.push(stringNode(naming.toDatabase(column)));
136
- return { ColumnRef: { fields } };
137
141
  }
138
- function columnRefStar(table, naming = identityNaming) {
139
- const fields = [];
140
- if (table) {
141
- fields.push(stringNode(naming.toDatabase(table)));
142
- }
143
- fields.push({ A_Star: {} });
144
- return { ColumnRef: { fields } };
142
+ function isReservedKeyword(value) {
143
+ return SQL_RESERVED_KEYWORDS.has(value.toLowerCase());
145
144
  }
146
- function rangeVar(table, alias, schema, naming = identityNaming) {
147
- const rv = {
148
- relname: naming.toDatabase(table),
149
- inh: true,
150
- relpersistence: "p"
151
- };
152
- if (schema) {
153
- rv.schemaname = naming.toDatabase(schema);
154
- }
155
- if (alias) {
156
- rv.alias = { aliasname: naming.toDatabase(alias) };
145
+ function validateQualifiedIdentifier(schemaTable) {
146
+ const parts = schemaTable.split(".");
147
+ if (parts.length === 1) {
148
+ validateIdentifier(parts[0], "table");
149
+ return { table: parts[0] };
157
150
  }
158
- return { RangeVar: rv };
159
- }
160
- function resTarget(val, name) {
161
- const rt = { val };
162
- if (name) {
163
- rt.name = name;
151
+ if (parts.length === 2) {
152
+ const schema = parts[0];
153
+ const table = parts[1];
154
+ validateIdentifier(schema, "schema");
155
+ validateIdentifier(table, "table");
156
+ return { schema, table };
164
157
  }
165
- return { ResTarget: rt };
166
- }
167
- function columnTarget(column, alias, table, naming = identityNaming) {
168
- return resTarget(columnRef(column, table, void 0, naming), alias);
169
- }
170
- function starTarget(table, naming = identityNaming) {
171
- return resTarget(columnRefStar(table, naming));
172
- }
173
- function binaryExpr(operator, left, right, kind = "AEXPR_OP") {
174
- const expr = {
175
- kind,
176
- name: [stringNode(operator)],
177
- lexpr: left,
178
- rexpr: right
179
- };
180
- return { A_Expr: expr };
181
- }
182
- function eqExpr(left, right) {
183
- return binaryExpr("=", left, right);
184
- }
185
- function fkCorrelation(col1, alias1, col2, alias2, naming) {
186
- return eqExpr(
187
- columnRef(col1, alias1, void 0, naming),
188
- columnRef(col2, alias2, void 0, naming)
158
+ throw new InvalidIdentifierError(
159
+ schemaTable,
160
+ "table",
161
+ "too many dots in qualified name (expected schema.table or table)"
189
162
  );
190
163
  }
191
- function neExpr(left, right) {
192
- return binaryExpr("<>", left, right);
193
- }
194
- function ltExpr(left, right) {
195
- return binaryExpr("<", left, right);
196
- }
197
- function lteExpr(left, right) {
198
- return binaryExpr("<=", left, right);
199
- }
200
- function gtExpr(left, right) {
201
- return binaryExpr(">", left, right);
202
- }
203
- function gteExpr(left, right) {
204
- return binaryExpr(">=", left, right);
205
- }
206
- function likeExpr(left, right) {
207
- return binaryExpr("~~", left, right, "AEXPR_LIKE");
208
- }
209
- function ilikeExpr(left, right) {
210
- return binaryExpr("~~*", left, right, "AEXPR_ILIKE");
211
- }
212
- function boolExpr(type, args) {
213
- const expr = {
214
- boolop: type,
215
- args
216
- };
217
- return { BoolExpr: expr };
218
- }
219
- function andExpr(...args) {
220
- return boolExpr("AND_EXPR", args);
221
- }
222
- function orExpr(...args) {
223
- return boolExpr("OR_EXPR", args);
164
+ function validateIdentifiers(identifiers) {
165
+ for (const [value, type] of Object.entries(identifiers)) {
166
+ if (value) {
167
+ validateIdentifier(value, type);
168
+ }
169
+ }
224
170
  }
225
- function notExpr(arg) {
226
- return boolExpr("NOT_EXPR", [arg]);
171
+ function sanitizeForDisplay(value) {
172
+ return value.replace(/[\x00-\x1f\x7f]/g, "?").slice(0, 100);
227
173
  }
228
- function typeCast(arg, typeName, isArray = false) {
229
- const tn = {
230
- names: [stringNode(typeName)],
231
- typemod: -1
232
- };
233
- if (isArray) {
234
- tn.arrayBounds = [integerNode(-1)];
174
+ function validateExtensionName(name, context = "extension") {
175
+ if (!name || name.length === 0) {
176
+ throw new InvalidIdentifierError(name, context, "cannot be empty");
235
177
  }
236
- const tc = {
237
- arg,
238
- typeName: tn
239
- };
240
- return { TypeCast: tc };
241
- }
242
- function funcCall(name, args = [], options = {}) {
243
- const names = Array.isArray(name) ? name.map(stringNode) : [stringNode(name)];
244
- const fc = {
245
- funcname: names
246
- };
247
- if (options.star) {
248
- fc.agg_star = true;
249
- } else if (args.length > 0) {
250
- fc.args = args;
178
+ if (name.length > 63) {
179
+ throw new InvalidIdentifierError(
180
+ name,
181
+ context,
182
+ `exceeds maximum length of 63 characters (got ${name.length})`
183
+ );
251
184
  }
252
- if (options.distinct) {
253
- fc.agg_distinct = true;
185
+ if (/[\x00-\x1f\x7f]/.test(name)) {
186
+ throw new InvalidIdentifierError(
187
+ name,
188
+ context,
189
+ "contains control characters"
190
+ );
254
191
  }
255
- if (options.orderBy && options.orderBy.length > 0) {
256
- fc.agg_order = options.orderBy;
192
+ if (/[\\]/.test(name)) {
193
+ throw new InvalidIdentifierError(
194
+ name,
195
+ context,
196
+ "contains backslash (forbidden in extension names)"
197
+ );
257
198
  }
258
- if (options.filter) {
259
- fc.agg_filter = options.filter;
199
+ if (/"/.test(name)) {
200
+ throw new InvalidIdentifierError(
201
+ name,
202
+ context,
203
+ "contains double-quote (identifier injection risk)"
204
+ );
260
205
  }
261
- return { FuncCall: fc };
262
- }
263
- function coalesceExpr(args) {
264
- return { CoalesceExpr: { args } };
265
- }
266
- function coalesce(...args) {
267
- return funcCall("coalesce", args);
268
- }
269
- function sortBy(expr, direction = "DEFAULT", nulls = "DEFAULT") {
270
- const sb = {
271
- node: expr,
272
- sortby_dir: direction === "ASC" ? "SORTBY_ASC" : direction === "DESC" ? "SORTBY_DESC" : "SORTBY_DEFAULT",
273
- sortby_nulls: nulls === "FIRST" ? "SORTBY_NULLS_FIRST" : nulls === "LAST" ? "SORTBY_NULLS_LAST" : "SORTBY_NULLS_DEFAULT"
274
- };
275
- return { SortBy: sb };
276
- }
277
- function joinExpr(joinType, left, right, quals, alias) {
278
- const je = {
279
- jointype: joinType,
280
- larg: left,
281
- rarg: right
282
- };
283
- if (quals) {
284
- je.quals = quals;
206
+ if (/'/.test(name)) {
207
+ throw new InvalidIdentifierError(
208
+ name,
209
+ context,
210
+ "contains single-quote (string injection risk)"
211
+ );
285
212
  }
286
- if (alias) {
287
- je.alias = { aliasname: alias };
213
+ if (/;/.test(name)) {
214
+ throw new InvalidIdentifierError(
215
+ name,
216
+ context,
217
+ "contains semicolon (statement injection risk)"
218
+ );
288
219
  }
289
- return { JoinExpr: je };
290
- }
291
- function innerJoin(left, right, on, alias) {
292
- return joinExpr("JOIN_INNER", left, right, on, alias);
293
- }
294
- function leftJoin(left, right, on, alias) {
295
- return joinExpr("JOIN_LEFT", left, right, on, alias);
296
- }
297
- function selectStmt(options) {
298
- const stmt = {
299
- targetList: options.targetList
300
- };
301
- if (options.from && options.from.length > 0) {
302
- stmt.fromClause = options.from;
220
+ if (/--/.test(name)) {
221
+ throw new InvalidIdentifierError(
222
+ name,
223
+ context,
224
+ "contains line-comment marker (--)"
225
+ );
303
226
  }
304
- if (options.where) {
305
- stmt.whereClause = options.where;
227
+ if (/\/\*/.test(name)) {
228
+ throw new InvalidIdentifierError(
229
+ name,
230
+ context,
231
+ "contains block-comment opener (/*)"
232
+ );
306
233
  }
307
- if (options.groupBy && options.groupBy.length > 0) {
308
- stmt.groupClause = options.groupBy;
234
+ if (/\*\//.test(name)) {
235
+ throw new InvalidIdentifierError(
236
+ name,
237
+ context,
238
+ "contains block-comment closer (*/)"
239
+ );
309
240
  }
310
- if (options.having) {
311
- stmt.havingClause = options.having;
241
+ if (/\$\$/.test(name)) {
242
+ throw new InvalidIdentifierError(
243
+ name,
244
+ context,
245
+ "contains dollar-quoting ($$)"
246
+ );
312
247
  }
313
- if (options.orderBy && options.orderBy.length > 0) {
314
- stmt.sortClause = options.orderBy;
248
+ if (/\s/.test(name)) {
249
+ throw new InvalidIdentifierError(name, context, "contains whitespace");
315
250
  }
316
- if (options.limit) {
317
- stmt.limitCount = options.limit;
251
+ if (!/^[a-zA-Z0-9_\-.]+$/.test(name)) {
252
+ throw new InvalidIdentifierError(
253
+ name,
254
+ context,
255
+ "contains characters not allowed in extension names (only letters, digits, underscore, hyphen, and dot allowed)"
256
+ );
318
257
  }
319
- if (options.offset) {
320
- stmt.limitOffset = options.offset;
258
+ }
259
+ function validateCollationName(name, context = "collation") {
260
+ if (!name || name.length === 0) {
261
+ throw new InvalidIdentifierError(name, context, "cannot be empty");
321
262
  }
322
- if (options.distinct === true) {
323
- stmt.distinctClause = [];
324
- } else if (Array.isArray(options.distinct) && options.distinct.length > 0) {
325
- stmt.distinctClause = options.distinct;
263
+ if (Buffer.byteLength(name, "utf8") > 63) {
264
+ throw new InvalidIdentifierError(
265
+ name,
266
+ context,
267
+ `exceeds maximum length of 63 bytes (got ${Buffer.byteLength(name, "utf8")})`
268
+ );
326
269
  }
327
- if (options.withClause && options.withClause.ctes.length > 0) {
328
- stmt.withClause = {
329
- ctes: options.withClause.ctes,
330
- recursive: options.withClause.recursive ?? false
331
- };
270
+ if (/\x00/.test(name)) {
271
+ throw new InvalidIdentifierError(
272
+ name,
273
+ context,
274
+ "contains NUL byte (\\x00) which would be silently truncated by PostgreSQL"
275
+ );
332
276
  }
333
- if (options.lockingClause) {
334
- const clause = {
335
- strength: options.lockingClause.strength,
336
- waitPolicy: options.lockingClause.waitPolicy ?? "LockWaitBlock"
337
- };
338
- if (options.lockingClause.lockedRels && options.lockingClause.lockedRels.length > 0) {
339
- clause.lockedRels = options.lockingClause.lockedRels;
340
- }
341
- stmt.lockingClause = [{ LockingClause: clause }];
277
+ if (/[\x01-\x1f\x7f]/.test(name)) {
278
+ throw new InvalidIdentifierError(
279
+ name,
280
+ context,
281
+ "contains control characters (only printable characters allowed)"
282
+ );
342
283
  }
343
- return { SelectStmt: stmt };
344
- }
345
- function mapLockToAst(lock) {
346
- return {
347
- strength: STRENGTH_MAP[lock.strength],
348
- waitPolicy: POLICY_MAP[lock.waitPolicy]
349
- };
350
- }
351
- function insertStmt(options) {
352
- const naming = options.naming ?? identityNaming;
353
- const relation = {
354
- relname: naming.toDatabase(options.table),
355
- inh: true,
356
- relpersistence: "p"
357
- };
358
- if (options.schema) {
359
- relation.schemaname = naming.toDatabase(options.schema);
284
+ if (/[\\]/.test(name)) {
285
+ throw new InvalidIdentifierError(
286
+ name,
287
+ context,
288
+ "contains backslash (forbidden in collation names)"
289
+ );
360
290
  }
361
- const stmt = {
362
- relation
363
- };
364
- if (options.columns && options.columns.length > 0) {
365
- stmt.cols = options.columns.map((col) => ({
366
- ResTarget: { name: naming.toDatabase(col) }
367
- }));
291
+ if (/"/.test(name)) {
292
+ throw new InvalidIdentifierError(
293
+ name,
294
+ context,
295
+ "contains double-quote (identifier injection risk)"
296
+ );
368
297
  }
369
- if (options.selectQuery) {
370
- stmt.selectStmt = options.selectQuery;
371
- } else if (options.values && options.values.length > 0) {
372
- stmt.selectStmt = {
373
- SelectStmt: {
374
- valuesLists: options.values.map((row) => ({ List: { items: row } }))
375
- }
376
- };
298
+ if (/'/.test(name)) {
299
+ throw new InvalidIdentifierError(
300
+ name,
301
+ context,
302
+ "contains single-quote (string injection risk)"
303
+ );
377
304
  }
378
- applyReturningList(stmt, options.returning);
379
- return { InsertStmt: stmt };
380
- }
381
- function updateStmt(options) {
382
- const naming = options.naming ?? identityNaming;
383
- const relation = {
384
- relname: naming.toDatabase(options.table),
385
- inh: true,
386
- relpersistence: "p"
387
- };
388
- if (options.schema) {
389
- relation.schemaname = naming.toDatabase(options.schema);
305
+ if (/;/.test(name)) {
306
+ throw new InvalidIdentifierError(
307
+ name,
308
+ context,
309
+ "contains semicolon (statement injection risk)"
310
+ );
390
311
  }
391
- const stmt = {
392
- relation,
393
- targetList: options.set.map(({ column, value }) => ({
394
- ResTarget: {
395
- name: naming.toDatabase(column),
396
- val: value
397
- }
398
- }))
399
- };
400
- if (options.where) {
401
- stmt.whereClause = options.where;
312
+ if (/--/.test(name)) {
313
+ throw new InvalidIdentifierError(
314
+ name,
315
+ context,
316
+ "contains line-comment marker (--)"
317
+ );
402
318
  }
403
- if (options.from && options.from.length > 0) {
404
- stmt.fromClause = options.from;
319
+ if (/\/\*/.test(name)) {
320
+ throw new InvalidIdentifierError(
321
+ name,
322
+ context,
323
+ "contains block-comment opener (/*)"
324
+ );
405
325
  }
406
- applyReturningList(stmt, options.returning);
407
- return { UpdateStmt: stmt };
408
- }
409
- function deleteStmt(options) {
410
- const naming = options.naming ?? identityNaming;
411
- const relation = {
412
- relname: naming.toDatabase(options.table),
413
- inh: true,
414
- relpersistence: "p"
415
- };
416
- if (options.schema) {
417
- relation.schemaname = naming.toDatabase(options.schema);
326
+ if (/\*\//.test(name)) {
327
+ throw new InvalidIdentifierError(
328
+ name,
329
+ context,
330
+ "contains block-comment closer (*/)"
331
+ );
418
332
  }
419
- const stmt = {
420
- relation
421
- };
422
- if (options.where) {
423
- stmt.whereClause = options.where;
333
+ if (/\$\$/.test(name)) {
334
+ throw new InvalidIdentifierError(
335
+ name,
336
+ context,
337
+ "contains dollar-quoting ($$)"
338
+ );
424
339
  }
425
- if (options.using && options.using.length > 0) {
426
- stmt.usingClause = options.using;
340
+ if (/\s/.test(name)) {
341
+ throw new InvalidIdentifierError(name, context, "contains whitespace");
342
+ }
343
+ if (!/^[a-zA-Z_][a-zA-Z0-9_.-]*(?:@[A-Za-z0-9-]{1,10})?$/.test(name)) {
344
+ throw new InvalidIdentifierError(
345
+ name,
346
+ context,
347
+ "contains characters not allowed in collation names (only letters, digits, underscore, hyphen, and dot allowed; optional @modifier must be 1-10 alphanumeric/hyphen characters, e.g. @euro, @latin9, @iso8859-15)"
348
+ );
427
349
  }
428
- applyReturningList(stmt, options.returning);
429
- return { DeleteStmt: stmt };
430
350
  }
431
- function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = identityNaming, options) {
432
- const targetAlias = options?.innerAlias ?? "__t__";
433
- const cols = options?.columns;
434
- const hasProjection = cols && cols.length > 0 && !(cols.length === 1 && cols[0] === "*");
435
- let toJsonbCall;
436
- if (hasProjection) {
437
- const projArgs = [];
438
- for (const col of cols) {
439
- projArgs.push({ A_Const: { sval: { sval: naming.toDatabase(col) } } });
440
- projArgs.push(columnRef(col, targetAlias, void 0, naming));
441
- }
442
- toJsonbCall = {
443
- FuncCall: {
444
- funcname: [stringNode("jsonb_build_object")],
445
- args: projArgs
446
- }
447
- };
448
- } else {
449
- const rowRef = {
450
- ColumnRef: {
451
- fields: [stringNode(targetAlias)]
452
- }
453
- };
454
- toJsonbCall = {
455
- FuncCall: {
456
- funcname: [stringNode("to_jsonb")],
457
- args: [rowRef]
458
- }
459
- };
351
+ function validateSqlExpression(sql, context) {
352
+ if (/[;]|--|\/\*|\*\/|\$\$|\\/.test(sql)) {
353
+ throw new Error(
354
+ `Unsafe SQL expression in ${context}: contains forbidden characters (;, --, /*, */, "$$" (dollar-quoted strings), \\). Value: "${sql}"`
355
+ );
460
356
  }
461
- if (options?.childNodes && options.childNodes.length > 0) {
462
- const buildObjectArgs = [];
463
- for (const child of options.childNodes) {
464
- buildObjectArgs.push({ A_Const: { sval: { sval: child.key } } });
465
- buildObjectArgs.push(child.node);
466
- }
467
- const jsonbBuildObject = {
468
- FuncCall: {
469
- funcname: [stringNode("jsonb_build_object")],
470
- args: buildObjectArgs
471
- }
472
- };
473
- toJsonbCall = {
474
- A_Expr: {
475
- kind: "AEXPR_OP",
476
- name: [stringNode("||")],
477
- lexpr: toJsonbCall,
478
- rexpr: jsonbBuildObject
357
+ }
358
+ function validateDbTypeName(type) {
359
+ if (!SAFE_TYPE_PATTERN.test(type)) {
360
+ throw new Error(
361
+ `Unsafe database type name: "${type}". Must match PostgreSQL type name rules.`
362
+ );
363
+ }
364
+ return type;
365
+ }
366
+ var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_TYPE_PATTERN;
367
+ var init_validate = __esm({
368
+ "src/validate.ts"() {
369
+ "use strict";
370
+ SQL_RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
371
+ "all",
372
+ "analyse",
373
+ "analyze",
374
+ "and",
375
+ "any",
376
+ "array",
377
+ "as",
378
+ "asc",
379
+ "asymmetric",
380
+ "authorization",
381
+ "binary",
382
+ "both",
383
+ "case",
384
+ "cast",
385
+ "check",
386
+ "collate",
387
+ "collation",
388
+ "column",
389
+ "concurrently",
390
+ "constraint",
391
+ "create",
392
+ "cross",
393
+ "current_catalog",
394
+ "current_date",
395
+ "current_role",
396
+ "current_schema",
397
+ "current_time",
398
+ "current_timestamp",
399
+ "current_user",
400
+ "default",
401
+ "deferrable",
402
+ "desc",
403
+ "distinct",
404
+ "do",
405
+ "else",
406
+ "end",
407
+ "except",
408
+ "false",
409
+ "fetch",
410
+ "for",
411
+ "foreign",
412
+ "freeze",
413
+ "from",
414
+ "full",
415
+ "grant",
416
+ "group",
417
+ "having",
418
+ "ilike",
419
+ "in",
420
+ "initially",
421
+ "inner",
422
+ "intersect",
423
+ "into",
424
+ "is",
425
+ "isnull",
426
+ "join",
427
+ "lateral",
428
+ "leading",
429
+ "left",
430
+ "like",
431
+ "limit",
432
+ "localtime",
433
+ "localtimestamp",
434
+ "natural",
435
+ "not",
436
+ "notnull",
437
+ "null",
438
+ "offset",
439
+ "on",
440
+ "only",
441
+ "or",
442
+ "order",
443
+ "outer",
444
+ "overlaps",
445
+ "placing",
446
+ "primary",
447
+ "references",
448
+ "returning",
449
+ "right",
450
+ "select",
451
+ "session_user",
452
+ "similar",
453
+ "some",
454
+ "symmetric",
455
+ "table",
456
+ "timestamp",
457
+ "tablesample",
458
+ "then",
459
+ "to",
460
+ "trailing",
461
+ "true",
462
+ "union",
463
+ "unique",
464
+ "user",
465
+ "using",
466
+ "variadic",
467
+ "verbose",
468
+ "when",
469
+ "where",
470
+ "window",
471
+ "with"
472
+ ]);
473
+ InvalidIdentifierError = class extends Error {
474
+ constructor(identifier, identifierType, reason) {
475
+ super(`Invalid ${identifierType} identifier "${identifier}": ${reason}`);
476
+ this.identifier = identifier;
477
+ this.identifierType = identifierType;
478
+ this.reason = reason;
479
+ this.name = "InvalidIdentifierError";
479
480
  }
481
+ identifier;
482
+ identifierType;
483
+ reason;
480
484
  };
485
+ SAFE_TYPE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/;
481
486
  }
482
- const jsonAggCall = {
483
- FuncCall: {
484
- funcname: [stringNode("json_agg")],
485
- args: [toJsonbCall]
486
- }
487
- };
488
- const fromTable = rangeVar(targetTable, targetAlias, schemaName, naming);
489
- const limitNode = options?.limit !== void 0 ? { A_Const: { ival: { ival: options.limit } } } : void 0;
490
- const innerSelect = selectStmt({
491
- targetList: [{ ResTarget: { val: jsonAggCall } }],
492
- from: [fromTable],
493
- where: whereExpr,
494
- ...limitNode && { limit: limitNode }
495
- });
496
- const subLink = {
497
- SubLink: {
498
- subLinkType: "EXPR_SUBLINK",
499
- // scalar subquery
500
- subselect: innerSelect
501
- }
502
- };
503
- const emptyArrayDefault = {
504
- TypeCast: {
505
- arg: { A_Const: { sval: { sval: "[]" } } },
506
- typeName: {
507
- names: [stringNode("json")],
508
- typemod: -1
509
- }
487
+ });
488
+
489
+ // src/ast-helpers.ts
490
+ import { normalizeSQL } from "@dbsp/core";
491
+ function applyReturningList(stmt, returning) {
492
+ if (returning && returning.length > 0) {
493
+ stmt.returningList = returning;
494
+ }
495
+ }
496
+ function stringNode(value) {
497
+ return { String: { sval: value } };
498
+ }
499
+ function integerNode(value) {
500
+ return { Integer: { ival: value } };
501
+ }
502
+ function floatNode(value) {
503
+ return { Float: { fval: value } };
504
+ }
505
+ function booleanConstNode(value) {
506
+ return {
507
+ A_Const: {
508
+ boolval: { boolval: value }
510
509
  }
511
510
  };
512
- const coalesceNode = coalesceExpr([subLink, emptyArrayDefault]);
511
+ }
512
+ function nullConstNode() {
513
513
  return {
514
- ResTarget: {
515
- val: coalesceNode,
516
- name: alias
514
+ A_Const: {
515
+ isnull: true
517
516
  }
518
517
  };
519
518
  }
520
- function jsonAggCorrelation(parentAlias, parentColumn, targetAlias, targetColumn, naming = identityNaming) {
521
- return eqExpr(
522
- columnRef(targetColumn, targetAlias, void 0, naming),
523
- columnRef(parentColumn, parentAlias, void 0, naming)
524
- );
525
- }
526
- var STRENGTH_MAP, POLICY_MAP;
527
- var init_ast_helpers = __esm({
528
- "src/ast-helpers.ts"() {
529
- "use strict";
530
- init_naming_plugin();
531
- STRENGTH_MAP = {
532
- forUpdate: "LCS_FORUPDATE",
533
- forNoKeyUpdate: "LCS_FORNOKEYUPDATE",
534
- forShare: "LCS_FORSHARE",
535
- forKeyShare: "LCS_FORKEYSHARE"
536
- };
537
- POLICY_MAP = {
538
- block: "LockWaitBlock",
539
- skipLocked: "LockWaitSkip",
540
- noWait: "LockWaitError"
541
- };
519
+ function columnRef(column, table, schema, naming = identityNaming) {
520
+ const fields = [];
521
+ if (schema) {
522
+ const dbSchema = naming.toDatabase(schema);
523
+ validateIdentifier(dbSchema, "schema");
524
+ fields.push(stringNode(dbSchema));
542
525
  }
543
- });
544
-
545
- // src/param-ref.ts
546
- function validateParamRef(paramRef) {
547
- const errors = [];
548
- if (paramRef.number === void 0) {
549
- errors.push("ParamRef.number is required");
550
- } else if (!Number.isInteger(paramRef.number)) {
551
- errors.push(`ParamRef.number must be an integer, got: ${paramRef.number}`);
552
- } else if (paramRef.number < 1) {
553
- errors.push(
554
- `ParamRef.number must be >= 1 (1-based indexing), got: ${paramRef.number}`
555
- );
556
- } else if (paramRef.number > 65535) {
557
- errors.push(
558
- `ParamRef.number exceeds maximum (65535), got: ${paramRef.number}`
559
- );
526
+ if (table) {
527
+ const dbTable = naming.toDatabase(table);
528
+ validateIdentifier(dbTable, "table");
529
+ fields.push(stringNode(dbTable));
560
530
  }
561
- return {
562
- valid: errors.length === 0,
563
- errors
564
- };
531
+ const dbColumn = naming.toDatabase(column);
532
+ if (dbColumn !== "*" && !/^\d+$/.test(dbColumn)) {
533
+ validateIdentifier(dbColumn, "column");
534
+ }
535
+ fields.push(stringNode(dbColumn));
536
+ return { ColumnRef: { fields } };
565
537
  }
566
- function createParamRef(number, location) {
567
- const paramRef = location !== void 0 ? { number, location } : { number };
568
- const result = validateParamRef(paramRef);
569
- if (!result.valid) {
570
- throw new Error(`Invalid ParamRef: ${result.errors.join(", ")}`);
538
+ function columnRefStar(table, naming = identityNaming) {
539
+ const fields = [];
540
+ if (table) {
541
+ fields.push(stringNode(naming.toDatabase(table)));
571
542
  }
572
- return { ParamRef: paramRef };
543
+ fields.push({ A_Star: {} });
544
+ return { ColumnRef: { fields } };
573
545
  }
574
- function createTypeCastParamRef(paramNumber, typeName, isArray = false, location) {
575
- const paramRefNode = createParamRef(paramNumber, location);
576
- const typeNameNode = isArray ? {
577
- names: [{ String: { sval: typeName } }],
578
- typemod: -1,
579
- arrayBounds: [{ Integer: { ival: -1 } }]
580
- } : {
581
- names: [{ String: { sval: typeName } }],
582
- typemod: -1
583
- };
584
- const typeCast2 = location !== void 0 ? {
585
- arg: paramRefNode,
586
- typeName: typeNameNode,
587
- location
588
- } : {
589
- arg: paramRefNode,
590
- typeName: typeNameNode
546
+ function rangeVar(table, alias, schema, naming = identityNaming) {
547
+ const dbTable = naming.toDatabase(table);
548
+ validateIdentifier(dbTable, "table");
549
+ const rv = {
550
+ relname: dbTable,
551
+ inh: true,
552
+ relpersistence: "p"
591
553
  };
592
- return { TypeCast: typeCast2 };
554
+ if (schema) {
555
+ const dbSchema = naming.toDatabase(schema);
556
+ validateIdentifier(dbSchema, "schema");
557
+ rv.schemaname = dbSchema;
558
+ }
559
+ if (alias) {
560
+ const dbAlias = naming.toDatabase(alias);
561
+ validateIdentifier(dbAlias, "alias");
562
+ rv.alias = { aliasname: dbAlias };
563
+ }
564
+ return { RangeVar: rv };
593
565
  }
594
- function createEqualityExpr(columnName, paramNumber, tableName, location) {
595
- const fields = tableName ? [{ String: { sval: tableName } }, { String: { sval: columnName } }] : [{ String: { sval: columnName } }];
596
- const kind = "AEXPR_OP";
597
- const expr = location !== void 0 ? {
598
- kind,
599
- name: [{ String: { sval: "=" } }],
600
- lexpr: { ColumnRef: { fields, location } },
601
- rexpr: createParamRef(paramNumber, location),
602
- location
603
- } : {
566
+ function resTarget(val, name) {
567
+ const rt = { val };
568
+ if (name) {
569
+ rt.name = name;
570
+ }
571
+ return { ResTarget: rt };
572
+ }
573
+ function columnTarget(column, alias, table, naming = identityNaming) {
574
+ return resTarget(columnRef(column, table, void 0, naming), alias);
575
+ }
576
+ function starTarget(table, naming = identityNaming) {
577
+ return resTarget(columnRefStar(table, naming));
578
+ }
579
+ function binaryExpr(operator, left, right, kind = "AEXPR_OP") {
580
+ const expr = {
604
581
  kind,
605
- name: [{ String: { sval: "=" } }],
606
- lexpr: { ColumnRef: { fields } },
607
- rexpr: createParamRef(paramNumber)
582
+ name: [stringNode(operator)],
583
+ lexpr: left,
584
+ rexpr: right
608
585
  };
609
586
  return { A_Expr: expr };
610
587
  }
611
- function createAnyExpr(columnName, paramNumber, tableName, location) {
612
- const fields = tableName ? [{ String: { sval: tableName } }, { String: { sval: columnName } }] : [{ String: { sval: columnName } }];
613
- const kind = "AEXPR_OP";
614
- const anyCall = location !== void 0 ? {
615
- funcname: [{ String: { sval: "any" } }],
616
- args: [createParamRef(paramNumber, location)],
617
- location
618
- } : {
619
- funcname: [{ String: { sval: "any" } }],
620
- args: [createParamRef(paramNumber)]
621
- };
622
- const expr = location !== void 0 ? {
623
- kind,
624
- name: [{ String: { sval: "=" } }],
625
- lexpr: { ColumnRef: { fields, location } },
626
- rexpr: { FuncCall: anyCall },
627
- location
628
- } : {
629
- kind,
630
- name: [{ String: { sval: "=" } }],
631
- lexpr: { ColumnRef: { fields } },
632
- rexpr: { FuncCall: anyCall }
588
+ function eqExpr(left, right) {
589
+ return binaryExpr("=", left, right);
590
+ }
591
+ function fkCorrelation(col1, alias1, col2, alias2, naming) {
592
+ return eqExpr(
593
+ columnRef(col1, alias1, void 0, naming),
594
+ columnRef(col2, alias2, void 0, naming)
595
+ );
596
+ }
597
+ function neExpr(left, right) {
598
+ return binaryExpr("<>", left, right);
599
+ }
600
+ function ltExpr(left, right) {
601
+ return binaryExpr("<", left, right);
602
+ }
603
+ function lteExpr(left, right) {
604
+ return binaryExpr("<=", left, right);
605
+ }
606
+ function gtExpr(left, right) {
607
+ return binaryExpr(">", left, right);
608
+ }
609
+ function gteExpr(left, right) {
610
+ return binaryExpr(">=", left, right);
611
+ }
612
+ function likeExpr(left, right) {
613
+ return binaryExpr("~~", left, right, "AEXPR_LIKE");
614
+ }
615
+ function ilikeExpr(left, right) {
616
+ return binaryExpr("~~*", left, right, "AEXPR_ILIKE");
617
+ }
618
+ function boolExpr(type, args) {
619
+ const expr = {
620
+ boolop: type,
621
+ args
633
622
  };
634
- return { A_Expr: expr };
623
+ return { BoolExpr: expr };
635
624
  }
636
- function collectAndValidateParamRefs(node) {
637
- const paramRefs = [];
638
- function traverse(n, path) {
639
- if (n === null || n === void 0) return;
640
- if (typeof n === "object") {
641
- const obj = n;
642
- if ("ParamRef" in obj && obj.ParamRef !== void 0) {
643
- paramRefs.push({
644
- paramRef: obj.ParamRef,
645
- path
646
- });
647
- }
648
- for (const [key, value] of Object.entries(obj)) {
649
- if (Array.isArray(value)) {
650
- value.forEach((item, index) => {
651
- traverse(item, `${path}.${key}[${index}]`);
652
- });
653
- } else if (typeof value === "object") {
654
- traverse(value, `${path}.${key}`);
655
- }
656
- }
657
- }
625
+ function andExpr(...args) {
626
+ return boolExpr("AND_EXPR", args);
627
+ }
628
+ function orExpr(...args) {
629
+ return boolExpr("OR_EXPR", args);
630
+ }
631
+ function notExpr(arg) {
632
+ return boolExpr("NOT_EXPR", [arg]);
633
+ }
634
+ function typeCast(arg, typeName, isArray = false) {
635
+ const tn = {
636
+ names: [stringNode(typeName)],
637
+ typemod: -1
638
+ };
639
+ if (isArray) {
640
+ tn.arrayBounds = [integerNode(-1)];
658
641
  }
659
- traverse(node, "root");
660
- const validationResults = paramRefs.map((p) => validateParamRef(p.paramRef));
661
- const allValid = validationResults.every((r) => r.valid);
662
- return { paramRefs, validationResults, allValid };
642
+ const tc = {
643
+ arg,
644
+ typeName: tn
645
+ };
646
+ return { TypeCast: tc };
663
647
  }
664
- var init_param_ref = __esm({
665
- "src/param-ref.ts"() {
666
- "use strict";
648
+ function funcCall(name, args = [], options = {}) {
649
+ const names = Array.isArray(name) ? name.map(stringNode) : [stringNode(name)];
650
+ const fc = {
651
+ funcname: names
652
+ };
653
+ if (options.star) {
654
+ fc.agg_star = true;
655
+ } else if (args.length > 0) {
656
+ fc.args = args;
667
657
  }
668
- });
669
-
670
- // src/handlers/expression/case-value.ts
671
- function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler) {
672
- if (value === null || value === void 0) {
673
- return nullConstNode();
658
+ if (options.distinct) {
659
+ fc.agg_distinct = true;
674
660
  }
675
- if (typeof value === "string") {
676
- return columnRef(value, alias, schema, naming);
661
+ if (options.orderBy && options.orderBy.length > 0) {
662
+ fc.agg_order = options.orderBy;
677
663
  }
678
- if (typeof value !== "object") {
679
- const idx = ++state.paramIndex;
680
- state.parameters.push(value);
681
- return createParamRef(idx);
664
+ if (options.filter) {
665
+ fc.agg_filter = options.filter;
682
666
  }
683
- const expr = value;
684
- switch (expr.kind) {
685
- case "literal":
686
- if (expr.value === null || expr.value === void 0)
687
- return nullConstNode();
688
- if (typeof expr.value === "boolean")
689
- return booleanConstNode(expr.value);
690
- if (typeof expr.value === "number") {
691
- if (Number.isInteger(expr.value))
692
- return integerNode(expr.value);
693
- return floatNode(String(expr.value));
694
- }
695
- {
696
- const idx = ++state.paramIndex;
697
- state.parameters.push(expr.value);
698
- return createParamRef(idx);
699
- }
700
- case "column":
701
- return columnRef(expr.column, alias, schema, naming);
702
- case "arithmetic": {
703
- const left = resolveCaseValue(
704
- expr.left,
705
- alias,
706
- schema,
707
- naming,
708
- state,
709
- nestedCaseHandler
710
- );
711
- const right = resolveCaseValue(
712
- expr.right,
713
- alias,
714
- schema,
715
- naming,
716
- state,
717
- nestedCaseHandler
718
- );
719
- return {
720
- A_Expr: {
721
- kind: "AEXPR_OP",
722
- name: [{ String: { sval: expr.operator } }],
723
- lexpr: left,
724
- rexpr: right
725
- }
726
- };
667
+ return { FuncCall: fc };
668
+ }
669
+ function coalesceExpr(args) {
670
+ return { CoalesceExpr: { args } };
671
+ }
672
+ function coalesce(...args) {
673
+ return funcCall("coalesce", args);
674
+ }
675
+ function sortBy(expr, direction = "DEFAULT", nulls = "DEFAULT") {
676
+ const sb = {
677
+ node: expr,
678
+ sortby_dir: direction === "ASC" ? "SORTBY_ASC" : direction === "DESC" ? "SORTBY_DESC" : "SORTBY_DEFAULT",
679
+ sortby_nulls: nulls === "FIRST" ? "SORTBY_NULLS_FIRST" : nulls === "LAST" ? "SORTBY_NULLS_LAST" : "SORTBY_NULLS_DEFAULT"
680
+ };
681
+ return { SortBy: sb };
682
+ }
683
+ function joinExpr(joinType, left, right, quals, alias) {
684
+ const je = {
685
+ jointype: joinType,
686
+ larg: left,
687
+ rarg: right
688
+ };
689
+ if (quals) {
690
+ je.quals = quals;
691
+ }
692
+ if (alias) {
693
+ je.alias = { aliasname: alias };
694
+ }
695
+ return { JoinExpr: je };
696
+ }
697
+ function innerJoin(left, right, on, alias) {
698
+ return joinExpr("JOIN_INNER", left, right, on, alias);
699
+ }
700
+ function leftJoin(left, right, on, alias) {
701
+ return joinExpr("JOIN_LEFT", left, right, on, alias);
702
+ }
703
+ function selectStmt(options) {
704
+ const stmt = {
705
+ targetList: options.targetList
706
+ };
707
+ if (options.from && options.from.length > 0) {
708
+ stmt.fromClause = options.from;
709
+ }
710
+ if (options.where) {
711
+ stmt.whereClause = options.where;
712
+ }
713
+ if (options.groupBy && options.groupBy.length > 0) {
714
+ stmt.groupClause = options.groupBy;
715
+ }
716
+ if (options.having) {
717
+ stmt.havingClause = options.having;
718
+ }
719
+ if (options.orderBy && options.orderBy.length > 0) {
720
+ stmt.sortClause = options.orderBy;
721
+ }
722
+ if (options.limit) {
723
+ stmt.limitCount = options.limit;
724
+ }
725
+ if (options.offset) {
726
+ stmt.limitOffset = options.offset;
727
+ }
728
+ if (options.distinct === true) {
729
+ stmt.distinctClause = [];
730
+ } else if (Array.isArray(options.distinct) && options.distinct.length > 0) {
731
+ stmt.distinctClause = options.distinct;
732
+ }
733
+ if (options.withClause && options.withClause.ctes.length > 0) {
734
+ stmt.withClause = {
735
+ ctes: options.withClause.ctes,
736
+ recursive: options.withClause.recursive ?? false
737
+ };
738
+ }
739
+ if (options.lockingClause) {
740
+ const clause = {
741
+ strength: options.lockingClause.strength,
742
+ waitPolicy: options.lockingClause.waitPolicy ?? "LockWaitBlock"
743
+ };
744
+ if (options.lockingClause.lockedRels && options.lockingClause.lockedRels.length > 0) {
745
+ clause.lockedRels = options.lockingClause.lockedRels;
727
746
  }
728
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional — no nested handler → parameterize via default
729
- case "case":
730
- if (nestedCaseHandler) {
731
- return nestedCaseHandler(expr);
747
+ stmt.lockingClause = [{ LockingClause: clause }];
748
+ }
749
+ return { SelectStmt: stmt };
750
+ }
751
+ function mapLockToAst(lock) {
752
+ return {
753
+ strength: STRENGTH_MAP[lock.strength],
754
+ waitPolicy: POLICY_MAP[lock.waitPolicy]
755
+ };
756
+ }
757
+ function insertStmt(options) {
758
+ const naming = options.naming ?? identityNaming;
759
+ const relation = {
760
+ relname: naming.toDatabase(options.table),
761
+ inh: true,
762
+ relpersistence: "p"
763
+ };
764
+ if (options.schema) {
765
+ relation.schemaname = naming.toDatabase(options.schema);
766
+ }
767
+ const stmt = {
768
+ relation
769
+ };
770
+ if (options.columns && options.columns.length > 0) {
771
+ stmt.cols = options.columns.map((col) => ({
772
+ ResTarget: { name: naming.toDatabase(col) }
773
+ }));
774
+ }
775
+ if (options.selectQuery) {
776
+ stmt.selectStmt = options.selectQuery;
777
+ } else if (options.values && options.values.length > 0) {
778
+ stmt.selectStmt = {
779
+ SelectStmt: {
780
+ valuesLists: options.values.map((row) => ({ List: { items: row } }))
732
781
  }
733
- // falls through
734
- default: {
735
- const idx = ++state.paramIndex;
736
- state.parameters.push(value);
737
- return createParamRef(idx);
738
- }
782
+ };
739
783
  }
784
+ applyReturningList(stmt, options.returning);
785
+ return { InsertStmt: stmt };
740
786
  }
741
- var init_case_value = __esm({
742
- "src/handlers/expression/case-value.ts"() {
743
- "use strict";
744
- init_ast_helpers();
745
- init_param_ref();
787
+ function updateStmt(options) {
788
+ const naming = options.naming ?? identityNaming;
789
+ const relation = {
790
+ relname: naming.toDatabase(options.table),
791
+ inh: true,
792
+ relpersistence: "p"
793
+ };
794
+ if (options.schema) {
795
+ relation.schemaname = naming.toDatabase(options.schema);
746
796
  }
747
- });
748
-
749
- // src/handlers/expression/custom.ts
750
- function registerWhereDispatcherFactory(factory) {
751
- _whereDispatcherFactory = factory;
797
+ const stmt = {
798
+ relation,
799
+ targetList: options.set.map(({ column, value }) => ({
800
+ ResTarget: {
801
+ name: naming.toDatabase(column),
802
+ val: value
803
+ }
804
+ }))
805
+ };
806
+ if (options.where) {
807
+ stmt.whereClause = options.where;
808
+ }
809
+ if (options.from && options.from.length > 0) {
810
+ stmt.fromClause = options.from;
811
+ }
812
+ applyReturningList(stmt, options.returning);
813
+ return { UpdateStmt: stmt };
752
814
  }
753
- function _createWhereDispatcher() {
754
- if (!_whereDispatcherFactory) {
755
- throw new Error(
756
- "compileExpressionIntent (case): WHERE dispatcher not initialized. Ensure compile-where.ts is imported before any CASE expression is compiled."
757
- );
815
+ function deleteStmt(options) {
816
+ const naming = options.naming ?? identityNaming;
817
+ const relation = {
818
+ relname: naming.toDatabase(options.table),
819
+ inh: true,
820
+ relpersistence: "p"
821
+ };
822
+ if (options.schema) {
823
+ relation.schemaname = naming.toDatabase(options.schema);
758
824
  }
759
- return _whereDispatcherFactory();
825
+ const stmt = {
826
+ relation
827
+ };
828
+ if (options.where) {
829
+ stmt.whereClause = options.where;
830
+ }
831
+ if (options.using && options.using.length > 0) {
832
+ stmt.usingClause = options.using;
833
+ }
834
+ applyReturningList(stmt, options.returning);
835
+ return { DeleteStmt: stmt };
760
836
  }
761
- function compileExpressionIntent(intent, ctx, state) {
762
- const kind = intent.kind;
763
- switch (kind) {
764
- case "customOp": {
765
- const i = intent;
766
- const leftNode = compileExpressionIntent(i.left, ctx, state);
767
- const rightNode = compileExpressionIntent(i.right, ctx, state);
768
- return {
769
- A_Expr: {
770
- kind: "AEXPR_OP",
771
- name: [{ String: { sval: i.operator } }],
772
- lexpr: leftNode,
773
- rexpr: rightNode
774
- }
775
- };
776
- }
777
- case "customFn": {
778
- const i = intent;
779
- const nameParts = i.name.split(".");
780
- const argNodes = i.args.map(
781
- (arg) => compileExpressionIntent(arg, ctx, state)
782
- );
783
- const orderByNodes = i.aggOrderBy && i.aggOrderBy.length > 0 ? i.aggOrderBy.map((ob) => {
784
- const colNode = columnRef(
785
- ob.field,
786
- void 0,
787
- void 0,
788
- ctx.naming
789
- );
790
- return sortBy(colNode, ob.direction === "desc" ? "DESC" : "ASC");
791
- }) : void 0;
792
- return funcCall(nameParts, argNodes, {
793
- ...orderByNodes ? { orderBy: orderByNodes } : {}
794
- });
795
- }
796
- case "ref": {
797
- const i = intent;
798
- const dotIdx = i.column.indexOf(".");
799
- if (dotIdx !== -1) {
800
- const table = i.column.slice(0, dotIdx);
801
- const col = i.column.slice(dotIdx + 1);
802
- return columnRef(col, table, void 0, ctx.naming);
803
- }
804
- return columnRef(i.column, void 0, void 0, ctx.naming);
837
+ function jsonAggSubquery(targetTable, whereExpr, alias, schemaName, naming = identityNaming, options) {
838
+ const targetAlias = options?.innerAlias ?? "__t__";
839
+ const cols = options?.columns;
840
+ const hasProjection = cols && cols.length > 0 && !(cols.length === 1 && cols[0] === "*");
841
+ let toJsonbCall;
842
+ if (hasProjection) {
843
+ const projArgs = [];
844
+ for (const col of cols) {
845
+ projArgs.push({ A_Const: { sval: { sval: naming.toDatabase(col) } } });
846
+ projArgs.push(columnRef(col, targetAlias, void 0, naming));
805
847
  }
806
- case "param": {
807
- const i = intent;
808
- const idx = ++state.paramIndex;
809
- state.parameters.push(i.value);
810
- return createParamRef(idx);
811
- }
812
- case "cast": {
813
- const i = intent;
814
- const argNode = compileExpressionIntent(i.expr, ctx, state);
815
- return typeCast(argNode, i.typeName);
816
- }
817
- case "literal": {
818
- const i = intent;
819
- if (i.value === null || i.value === void 0) {
820
- return nullConstNode();
821
- }
822
- if (typeof i.value === "boolean") {
823
- return booleanConstNode(i.value);
848
+ toJsonbCall = {
849
+ FuncCall: {
850
+ funcname: [stringNode("jsonb_build_object")],
851
+ args: projArgs
824
852
  }
825
- if (typeof i.value === "number") {
826
- if (Number.isInteger(i.value)) {
827
- return integerNode(i.value);
828
- }
829
- return floatNode(String(i.value));
853
+ };
854
+ } else {
855
+ const rowRef = {
856
+ ColumnRef: {
857
+ fields: [stringNode(targetAlias)]
830
858
  }
831
- if (typeof i.value === "string") {
832
- return {
833
- A_Const: { sval: { sval: i.value } }
834
- };
859
+ };
860
+ toJsonbCall = {
861
+ FuncCall: {
862
+ funcname: [stringNode("to_jsonb")],
863
+ args: [rowRef]
835
864
  }
836
- return {
837
- A_Const: { sval: { sval: String(i.value) } }
838
- };
839
- }
840
- case "unary": {
841
- const i = intent;
842
- const operandNode = compileExpressionIntent(i.operand, ctx, state);
843
- return {
844
- A_Expr: {
845
- kind: "AEXPR_OP",
846
- name: [{ String: { sval: i.operator } }],
847
- rexpr: operandNode
848
- }
849
- };
850
- }
851
- case "namedArg": {
852
- const nae = intent;
853
- const argNode = compileExpressionIntent(nae.value, ctx, state);
854
- return {
855
- NamedArgExpr: {
856
- arg: argNode,
857
- name: nae.name,
858
- argnumber: -1
859
- }
860
- };
861
- }
862
- case "star":
863
- return {
864
- ColumnRef: { fields: [{ A_Star: {} }] }
865
- };
866
- case "array": {
867
- const ae = intent;
868
- const elements = ae.elements.map(
869
- (el) => compileExpressionIntent(el, ctx, state)
870
- );
871
- return { A_ArrayExpr: { elements } };
865
+ };
866
+ }
867
+ if (options?.childNodes && options.childNodes.length > 0) {
868
+ const buildObjectArgs = [];
869
+ for (const child of options.childNodes) {
870
+ buildObjectArgs.push({ A_Const: { sval: { sval: child.key } } });
871
+ buildObjectArgs.push(child.node);
872
872
  }
873
- case "subquery": {
874
- const sq = intent;
875
- if (!ctx.compileSubquery) {
876
- throw new Error(
877
- "compileExpressionIntent: 'subquery' expression kind requires ctx.compileSubquery to be set. Use asExpr() only in .columns() context, not in standalone expressions."
878
- );
873
+ const jsonbBuildObject = {
874
+ FuncCall: {
875
+ funcname: [stringNode("jsonb_build_object")],
876
+ args: buildObjectArgs
879
877
  }
880
- const { ast: innerAst, parameters: innerParams } = ctx.compileSubquery(
881
- sq.query,
882
- state.paramIndex
883
- );
884
- for (const p of innerParams) {
885
- state.parameters.push(p);
878
+ };
879
+ toJsonbCall = {
880
+ A_Expr: {
881
+ kind: "AEXPR_OP",
882
+ name: [stringNode("||")],
883
+ lexpr: toJsonbCall,
884
+ rexpr: jsonbBuildObject
886
885
  }
887
- state.paramIndex += innerParams.length;
888
- return {
889
- SubLink: {
890
- subLinkType: "EXPR_SUBLINK",
891
- subselect: innerAst
892
- }
893
- };
886
+ };
887
+ }
888
+ const jsonAggCall = {
889
+ FuncCall: {
890
+ funcname: [stringNode("json_agg")],
891
+ args: [toJsonbCall]
894
892
  }
895
- case "relationColumn": {
896
- const rc = intent;
897
- const alias = state.aliases.get(rc.relation) ?? rc.relation;
898
- return columnRef(rc.column, alias, void 0, ctx.naming);
893
+ };
894
+ const fromTable = rangeVar(targetTable, targetAlias, schemaName, naming);
895
+ const limitNode = options?.limit !== void 0 ? { A_Const: { ival: { ival: options.limit } } } : void 0;
896
+ const innerSelect = selectStmt({
897
+ targetList: [{ ResTarget: { val: jsonAggCall } }],
898
+ from: [fromTable],
899
+ where: whereExpr,
900
+ ...limitNode && { limit: limitNode }
901
+ });
902
+ const subLink = {
903
+ SubLink: {
904
+ subLinkType: "EXPR_SUBLINK",
905
+ // scalar subquery
906
+ subselect: innerSelect
899
907
  }
900
- case "case": {
901
- const dispatch = _createWhereDispatcher();
902
- const caseIntent = intent;
903
- if (!caseIntent.when || caseIntent.when.length === 0) {
904
- throw new Error("CASE expression requires at least one WHEN clause");
905
- }
906
- const caseArgs = caseIntent.when.map((branch) => {
907
- const whenNode = dispatch(
908
- branch.condition,
909
- ctx,
910
- state
911
- );
912
- const thenNode = compileExpressionIntent(branch.result, ctx, state);
913
- return {
914
- CaseWhen: { expr: whenNode, result: thenNode }
915
- };
916
- });
917
- let defresult;
918
- if (caseIntent.else !== void 0) {
919
- defresult = compileExpressionIntent(caseIntent.else, ctx, state);
908
+ };
909
+ const emptyArrayDefault = {
910
+ TypeCast: {
911
+ arg: { A_Const: { sval: { sval: "[]" } } },
912
+ typeName: {
913
+ names: [stringNode("json")],
914
+ typemod: -1
920
915
  }
921
- return {
922
- CaseExpr: {
923
- args: caseArgs,
924
- ...defresult !== void 0 ? { defresult } : {}
925
- }
926
- };
927
916
  }
928
- default: {
929
- throw new Error(
930
- `compileExpressionIntent: unsupported expression kind '${kind}'`
931
- );
917
+ };
918
+ const coalesceNode = coalesceExpr([subLink, emptyArrayDefault]);
919
+ return {
920
+ ResTarget: {
921
+ val: coalesceNode,
922
+ name: alias
932
923
  }
933
- }
924
+ };
934
925
  }
935
- var _whereDispatcherFactory, customExpressionHandler;
936
- var init_custom = __esm({
937
- "src/handlers/expression/custom.ts"() {
926
+ function jsonAggCorrelation(parentAlias, parentColumn, targetAlias, targetColumn, naming = identityNaming) {
927
+ return eqExpr(
928
+ columnRef(targetColumn, targetAlias, void 0, naming),
929
+ columnRef(parentColumn, parentAlias, void 0, naming)
930
+ );
931
+ }
932
+ var STRENGTH_MAP, POLICY_MAP;
933
+ var init_ast_helpers = __esm({
934
+ "src/ast-helpers.ts"() {
938
935
  "use strict";
939
- init_ast_helpers();
940
- init_param_ref();
941
- customExpressionHandler = {
942
- types: [
943
- "customOp",
944
- "customFn",
945
- "ref",
946
- "param",
947
- "cast",
948
- "unary",
949
- "customExpression"
950
- ],
951
- compile(decision, ctx, state) {
952
- const expressionIntent = decision.expressionIntent;
953
- return compileExpressionIntent(expressionIntent, ctx, state);
954
- }
936
+ init_naming_plugin();
937
+ init_validate();
938
+ STRENGTH_MAP = {
939
+ forUpdate: "LCS_FORUPDATE",
940
+ forNoKeyUpdate: "LCS_FORNOKEYUPDATE",
941
+ forShare: "LCS_FORSHARE",
942
+ forKeyShare: "LCS_FORKEYSHARE"
943
+ };
944
+ POLICY_MAP = {
945
+ block: "LockWaitBlock",
946
+ skipLocked: "LockWaitSkip",
947
+ noWait: "LockWaitError"
955
948
  };
956
949
  }
957
950
  });
958
951
 
959
- // src/select-expression-handlers.ts
960
- function handleColumnExpression(expr, rootTable, decisions) {
961
- const decision = {
962
- type: "select",
963
- column: expr.column,
964
- table: rootTable
952
+ // src/param-ref.ts
953
+ function validateParamRef(paramRef) {
954
+ const errors = [];
955
+ if (paramRef.number === void 0) {
956
+ errors.push("ParamRef.number is required");
957
+ } else if (!Number.isInteger(paramRef.number)) {
958
+ errors.push(`ParamRef.number must be an integer, got: ${paramRef.number}`);
959
+ } else if (paramRef.number < 1) {
960
+ errors.push(
961
+ `ParamRef.number must be >= 1 (1-based indexing), got: ${paramRef.number}`
962
+ );
963
+ } else if (paramRef.number > 65535) {
964
+ errors.push(
965
+ `ParamRef.number exceeds maximum (65535), got: ${paramRef.number}`
966
+ );
967
+ }
968
+ return {
969
+ valid: errors.length === 0,
970
+ errors
965
971
  };
966
- const alias = expr.as ?? expr.alias;
967
- if (alias) decision.alias = alias;
968
- decisions.push(decision);
969
972
  }
970
- function handleAggregateExpression(expr, rootTable, decisions, applyFilter) {
971
- const aggFunc = expr.function;
972
- const aggField = expr.field;
973
- const aggAs = expr.as;
974
- const aggDistinct = expr.distinct;
975
- const aggFilter = expr.filter;
976
- if (aggFunc === "count" && !aggField) {
977
- const decision = {
978
- type: "selectFunction",
979
- function: "count",
980
- column: "*",
981
- table: rootTable
982
- };
983
- if (aggAs) decision.alias = aggAs;
984
- applyFilter(decision, aggFilter, rootTable);
985
- decisions.push(decision);
986
- } else if (aggFunc === "count" && aggDistinct && aggField) {
987
- const decision = {
988
- type: "selectFunction",
989
- function: "countDistinct",
990
- column: aggField,
991
- table: rootTable
992
- };
993
- if (aggAs) decision.alias = aggAs;
994
- applyFilter(decision, aggFilter, rootTable);
995
- decisions.push(decision);
996
- } else {
997
- const decision = {
998
- type: "selectFunction",
999
- function: aggFunc,
1000
- table: rootTable
1001
- };
1002
- if (aggField) decision.column = aggField;
1003
- if (aggAs) decision.alias = aggAs;
1004
- applyFilter(decision, aggFilter, rootTable);
1005
- decisions.push(decision);
973
+ function createParamRef(number, location) {
974
+ const paramRef = location !== void 0 ? { number, location } : { number };
975
+ const result = validateParamRef(paramRef);
976
+ if (!result.valid) {
977
+ throw new Error(`Invalid ParamRef: ${result.errors.join(", ")}`);
1006
978
  }
979
+ return { ParamRef: paramRef };
1007
980
  }
1008
- function handleCoalesceExpression(expr, rootTable, decisions) {
1009
- const decision = {
1010
- type: "selectFunction",
1011
- function: "coalesce",
1012
- args: expr.fields,
1013
- table: rootTable
1014
- };
1015
- if (expr.as) decision.alias = expr.as;
1016
- decisions.push(decision);
1017
- }
1018
- function handleRawExpression(expr, rootTable, decisions) {
1019
- const decision = {
1020
- type: "selectFunction",
1021
- function: "raw",
1022
- args: [expr.sql],
1023
- table: rootTable
1024
- };
1025
- if (expr.as) decision.alias = expr.as;
1026
- decisions.push(decision);
1027
- }
1028
- function handleWindowExpression(expr, rootTable, decisions) {
1029
- const windowFunc = expr.function;
1030
- const windowAlias = expr.alias;
1031
- const windowField = expr.field;
1032
- const over = expr.over;
1033
- const decision = {
1034
- type: "selectWindow",
1035
- function: windowFunc,
1036
- alias: windowAlias,
1037
- table: rootTable
1038
- };
1039
- if (windowField) decision.field = windowField;
1040
- if (over.partitionBy) decision.partitionBy = over.partitionBy;
1041
- if (over.orderBy) decision.orderBy = over.orderBy;
1042
- const windowOffset = expr.offset;
1043
- const windowDefault = expr.defaultValue;
1044
- if (windowOffset !== void 0) decision.args = [windowOffset];
1045
- if (windowDefault !== void 0) decision.value = windowDefault;
1046
- decisions.push(decision);
1047
- }
1048
- function handleCaseExpression(expr, rootTable, decisions, _applyFilter, convertCondition) {
1049
- const whenClauses = expr.when;
1050
- const conditions = whenClauses.map((wc) => ({
1051
- when: convertCondition(wc.condition, rootTable),
1052
- // biome-ignore lint/suspicious/noThenProperty: intentional reserved word in decision object
1053
- then: wc.result
1054
- }));
1055
- const decision = {
1056
- type: "selectExpression",
1057
- expressionType: "case",
1058
- table: rootTable
981
+ function createTypeCastParamRef(paramNumber, typeName, isArray = false, location) {
982
+ const paramRefNode = createParamRef(paramNumber, location);
983
+ const typeNameNode = isArray ? {
984
+ names: [{ String: { sval: typeName } }],
985
+ typemod: -1,
986
+ arrayBounds: [{ Integer: { ival: -1 } }]
987
+ } : {
988
+ names: [{ String: { sval: typeName } }],
989
+ typemod: -1
1059
990
  };
1060
- decision.conditions = conditions;
1061
- if (expr.else) {
1062
- decision.value = expr.else;
1063
- }
1064
- if (expr.as) decision.alias = expr.as;
1065
- decisions.push(decision);
1066
- }
1067
- function handleRelationColumnExpression(expr, rootTable, decisions) {
1068
- const decision = {
1069
- type: "selectRelationColumn",
1070
- relation: expr.relation,
1071
- column: expr.column ?? "*",
1072
- table: rootTable
991
+ const typeCast2 = location !== void 0 ? {
992
+ arg: paramRefNode,
993
+ typeName: typeNameNode,
994
+ location
995
+ } : {
996
+ arg: paramRefNode,
997
+ typeName: typeNameNode
1073
998
  };
1074
- if (expr.as) decision.alias = expr.as;
1075
- decisions.push(decision);
999
+ return { TypeCast: typeCast2 };
1076
1000
  }
1077
- function handleArithmeticExpression(expr, rootTable, decisions) {
1078
- const decision = {
1079
- type: "selectArithmetic",
1080
- operator: expr.operator,
1081
- args: [expr.left, expr.right],
1082
- table: rootTable
1001
+ function createEqualityExpr(columnName, paramNumber, tableName, location) {
1002
+ const fields = tableName ? [{ String: { sval: tableName } }, { String: { sval: columnName } }] : [{ String: { sval: columnName } }];
1003
+ const kind = "AEXPR_OP";
1004
+ const expr = location !== void 0 ? {
1005
+ kind,
1006
+ name: [{ String: { sval: "=" } }],
1007
+ lexpr: { ColumnRef: { fields, location } },
1008
+ rexpr: createParamRef(paramNumber, location),
1009
+ location
1010
+ } : {
1011
+ kind,
1012
+ name: [{ String: { sval: "=" } }],
1013
+ lexpr: { ColumnRef: { fields } },
1014
+ rexpr: createParamRef(paramNumber)
1083
1015
  };
1084
- if (expr.as) decision.alias = expr.as;
1085
- decisions.push(decision);
1016
+ return { A_Expr: expr };
1086
1017
  }
1087
- function handleJsonExtractExpression(expr, rootTable, decisions) {
1088
- const decision = {
1089
- type: "selectFunction",
1090
- function: "jsonExtract",
1091
- column: expr.field,
1092
- args: expr.path,
1093
- table: rootTable
1018
+ function createAnyExpr(columnName, paramNumber, tableName, location) {
1019
+ const fields = tableName ? [{ String: { sval: tableName } }, { String: { sval: columnName } }] : [{ String: { sval: columnName } }];
1020
+ const kind = "AEXPR_OP";
1021
+ const anyCall = location !== void 0 ? {
1022
+ funcname: [{ String: { sval: "any" } }],
1023
+ args: [createParamRef(paramNumber, location)],
1024
+ location
1025
+ } : {
1026
+ funcname: [{ String: { sval: "any" } }],
1027
+ args: [createParamRef(paramNumber)]
1094
1028
  };
1095
- if (expr.mode) decision.jsonMode = expr.mode;
1096
- if (expr.as) decision.alias = expr.as;
1097
- decisions.push(decision);
1098
- }
1099
- function handleJsonPathExtractExpression(expr, rootTable, decisions) {
1100
- const decision = {
1101
- type: "selectFunction",
1102
- function: "jsonPathExtract",
1103
- column: expr.field,
1104
- args: [expr.path],
1105
- table: rootTable
1029
+ const expr = location !== void 0 ? {
1030
+ kind,
1031
+ name: [{ String: { sval: "=" } }],
1032
+ lexpr: { ColumnRef: { fields, location } },
1033
+ rexpr: { FuncCall: anyCall },
1034
+ location
1035
+ } : {
1036
+ kind,
1037
+ name: [{ String: { sval: "=" } }],
1038
+ lexpr: { ColumnRef: { fields } },
1039
+ rexpr: { FuncCall: anyCall }
1106
1040
  };
1107
- if (expr.mode) decision.jsonMode = expr.mode;
1108
- if (expr.as) decision.alias = expr.as;
1109
- decisions.push(decision);
1041
+ return { A_Expr: expr };
1110
1042
  }
1111
- function handleCustomExpressionSelect(expr, rootTable, decisions) {
1112
- const decision = {
1113
- type: "selectCustomExpression",
1114
- expressionIntent: expr,
1115
- table: rootTable
1116
- };
1117
- const explicitAlias = expr.as;
1118
- if (explicitAlias) {
1119
- decision.alias = explicitAlias;
1120
- } else if (expr.kind === "cast") {
1121
- const inner = expr.expr;
1122
- if (inner?.kind === "ref") {
1123
- const col = inner.column;
1124
- const dotIdx = col.lastIndexOf(".");
1125
- decision.alias = dotIdx !== -1 ? col.slice(dotIdx + 1) : col;
1043
+ function collectAndValidateParamRefs(node) {
1044
+ const paramRefs = [];
1045
+ function traverse(n, path) {
1046
+ if (n === null || n === void 0) return;
1047
+ if (typeof n === "object") {
1048
+ const obj = n;
1049
+ if ("ParamRef" in obj && obj.ParamRef !== void 0) {
1050
+ paramRefs.push({
1051
+ paramRef: obj.ParamRef,
1052
+ path
1053
+ });
1054
+ }
1055
+ for (const [key, value] of Object.entries(obj)) {
1056
+ if (Array.isArray(value)) {
1057
+ value.forEach((item, index) => {
1058
+ traverse(item, `${path}.${key}[${index}]`);
1059
+ });
1060
+ } else if (typeof value === "object") {
1061
+ traverse(value, `${path}.${key}`);
1062
+ }
1063
+ }
1126
1064
  }
1127
1065
  }
1128
- decisions.push(decision);
1066
+ traverse(node, "root");
1067
+ const validationResults = paramRefs.map((p) => validateParamRef(p.paramRef));
1068
+ const allValid = validationResults.every((r) => r.valid);
1069
+ return { paramRefs, validationResults, allValid };
1129
1070
  }
1130
- var EXPRESSION_HANDLERS;
1131
- var init_select_expression_handlers = __esm({
1132
- "src/select-expression-handlers.ts"() {
1071
+ var init_param_ref = __esm({
1072
+ "src/param-ref.ts"() {
1133
1073
  "use strict";
1134
- EXPRESSION_HANDLERS = {
1135
- column: (expr, rootTable, decisions) => handleColumnExpression(expr, rootTable, decisions),
1136
- columnAlias: (expr, rootTable, decisions) => handleColumnExpression(expr, rootTable, decisions),
1137
- aggregate: (expr, rootTable, decisions, applyFilter) => handleAggregateExpression(expr, rootTable, decisions, applyFilter),
1138
- coalesce: (expr, rootTable, decisions) => handleCoalesceExpression(expr, rootTable, decisions),
1139
- raw: (expr, rootTable, decisions) => handleRawExpression(expr, rootTable, decisions),
1140
- window: (expr, rootTable, decisions) => handleWindowExpression(expr, rootTable, decisions),
1141
- case: (expr, rootTable, decisions, applyFilter, convertCondition) => handleCaseExpression(
1142
- expr,
1143
- rootTable,
1144
- decisions,
1145
- applyFilter,
1146
- convertCondition
1147
- ),
1148
- relationColumn: (expr, rootTable, decisions) => handleRelationColumnExpression(expr, rootTable, decisions),
1149
- // pseudoColumn: intentional no-op — planner hints, not SQL
1150
- arithmetic: (expr, rootTable, decisions) => handleArithmeticExpression(expr, rootTable, decisions),
1151
- jsonExtract: (expr, rootTable, decisions) => handleJsonExtractExpression(expr, rootTable, decisions),
1152
- jsonPathExtract: (expr, rootTable, decisions) => handleJsonPathExtractExpression(expr, rootTable, decisions),
1153
- // Custom expression types — produce 'selectCustomExpression' decisions
1154
- literal: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1155
- customOp: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1156
- customFn: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1157
- ref: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1158
- param: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1159
- cast: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1160
- unary: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions)
1161
- };
1162
1074
  }
1163
1075
  });
1164
1076
 
1165
- // src/intent-to-decisions.ts
1166
- function intentToDecisions(intent, rootTable) {
1167
- const decisions = [];
1168
- if (intent.select) {
1169
- decisions.push(...convertSelect(intent.select, rootTable));
1170
- } else {
1171
- decisions.push({ type: "select", column: "*", table: rootTable });
1077
+ // src/handlers/expression/case-value.ts
1078
+ function resolveCaseValue(value, alias, schema, naming, state, nestedCaseHandler) {
1079
+ if (value === null || value === void 0) {
1080
+ return nullConstNode();
1172
1081
  }
1173
- if (intent.where) {
1174
- decisions.push(...convertWhere(intent.where, rootTable));
1082
+ if (typeof value === "string") {
1083
+ return columnRef(value, alias, schema, naming);
1175
1084
  }
1176
- if (intent.orderBy && intent.orderBy.length > 0) {
1177
- for (const order of intent.orderBy) {
1178
- decisions.push(convertOrderBy(order, rootTable));
1179
- }
1085
+ if (typeof value !== "object") {
1086
+ const idx = ++state.paramIndex;
1087
+ state.parameters.push(value);
1088
+ return createParamRef(idx);
1180
1089
  }
1181
- if (intent.groupBy && intent.groupBy.length > 0) {
1182
- for (const col of intent.groupBy) {
1183
- decisions.push({ type: "groupBy", column: col, table: rootTable });
1090
+ const expr = value;
1091
+ switch (expr.kind) {
1092
+ case "literal":
1093
+ if (expr.value === null || expr.value === void 0)
1094
+ return nullConstNode();
1095
+ if (typeof expr.value === "boolean")
1096
+ return booleanConstNode(expr.value);
1097
+ if (typeof expr.value === "number") {
1098
+ if (Number.isInteger(expr.value))
1099
+ return integerNode(expr.value);
1100
+ return floatNode(String(expr.value));
1101
+ }
1102
+ {
1103
+ const idx = ++state.paramIndex;
1104
+ state.parameters.push(expr.value);
1105
+ return createParamRef(idx);
1106
+ }
1107
+ case "column":
1108
+ return columnRef(expr.column, alias, schema, naming);
1109
+ case "arithmetic": {
1110
+ const left = resolveCaseValue(
1111
+ expr.left,
1112
+ alias,
1113
+ schema,
1114
+ naming,
1115
+ state,
1116
+ nestedCaseHandler
1117
+ );
1118
+ const right = resolveCaseValue(
1119
+ expr.right,
1120
+ alias,
1121
+ schema,
1122
+ naming,
1123
+ state,
1124
+ nestedCaseHandler
1125
+ );
1126
+ return {
1127
+ A_Expr: {
1128
+ kind: "AEXPR_OP",
1129
+ name: [{ String: { sval: expr.operator } }],
1130
+ lexpr: left,
1131
+ rexpr: right
1132
+ }
1133
+ };
1184
1134
  }
1185
- }
1186
- if (intent.having) {
1187
- const havingDecisions = convertWhere(intent.having, rootTable);
1188
- for (const d of havingDecisions) {
1189
- decisions.push({ ...d, type: "having" });
1135
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional — no nested handler → parameterize via default
1136
+ case "case":
1137
+ if (nestedCaseHandler) {
1138
+ return nestedCaseHandler(expr);
1139
+ }
1140
+ // falls through
1141
+ default: {
1142
+ const idx = ++state.paramIndex;
1143
+ state.parameters.push(value);
1144
+ return createParamRef(idx);
1190
1145
  }
1191
1146
  }
1192
- if (intent.distinctOn && intent.distinctOn.length > 0) {
1193
- decisions.push({ type: "distinctOn", columns: intent.distinctOn });
1194
- } else if (intent.distinct) {
1195
- decisions.push({ type: "distinct" });
1196
- }
1197
- if (intent.limit !== void 0) {
1198
- decisions.push({ type: "limit", limit: intent.limit });
1199
- }
1200
- if (intent.offset !== void 0) {
1201
- decisions.push({ type: "offset", offset: intent.offset });
1147
+ }
1148
+ var init_case_value = __esm({
1149
+ "src/handlers/expression/case-value.ts"() {
1150
+ "use strict";
1151
+ init_ast_helpers();
1152
+ init_param_ref();
1202
1153
  }
1203
- return decisions;
1154
+ });
1155
+
1156
+ // src/handlers/expression/custom.ts
1157
+ import { validateTypeName } from "@dbsp/core";
1158
+ function registerWhereDispatcherFactory(factory) {
1159
+ _whereDispatcherFactory = factory;
1204
1160
  }
1205
- function applyFilterCondition(decision, filter, rootTable) {
1206
- if (filter) {
1207
- const filterDecision = convertWhereCondition(filter, rootTable);
1208
- if (filterDecision) decision.filterCondition = filterDecision;
1161
+ function _createWhereDispatcher() {
1162
+ if (!_whereDispatcherFactory) {
1163
+ throw new Error(
1164
+ "compileExpressionIntent (case): WHERE dispatcher not initialized. Ensure compile-where.ts is imported before any CASE expression is compiled."
1165
+ );
1209
1166
  }
1167
+ return _whereDispatcherFactory();
1210
1168
  }
1211
- function convertSelect(select, rootTable) {
1212
- const selectType = "type" in select ? select.type : void 0;
1213
- if ("all" in select && select.all === true) {
1214
- return [{ type: "select", column: "*", table: rootTable }];
1169
+ function assertSafeOperator(op3, opts) {
1170
+ if (typeof op3 !== "string") {
1171
+ throw new Error(
1172
+ `Invalid operator: expected a string, got ${typeof op3}. Operator must be a plain string value.`
1173
+ );
1215
1174
  }
1216
- if (selectType === "fields" && "fields" in select) {
1217
- return select.fields.map((field) => ({
1218
- type: "select",
1219
- column: field,
1220
- table: rootTable
1221
- }));
1175
+ if (!op3) {
1176
+ throw new Error(`Invalid operator "${op3}". Operator must not be empty.`);
1222
1177
  }
1223
- if (selectType === "expressions" && "columns" in select) {
1224
- const decisions = [];
1225
- const columns = select.columns;
1226
- for (const exprUnknown of columns) {
1227
- const expr = exprUnknown;
1228
- const handler = EXPRESSION_HANDLERS[expr.kind];
1229
- if (handler) {
1230
- handler(
1231
- expr,
1232
- rootTable,
1233
- decisions,
1234
- applyFilterCondition,
1235
- convertWhereCondition
1178
+ const SYMBOLIC_RE = /^[-+*/<>=~!@#%^&|?]+$/;
1179
+ const isSymbolic = SYMBOLIC_RE.test(op3);
1180
+ const isAllowedWord = opts?.allowWords?.some((w) => w.toLowerCase() === op3.toLowerCase()) ?? false;
1181
+ if (!isSymbolic && !isAllowedWord) {
1182
+ throw new Error(
1183
+ `Invalid operator "${op3}". Operator must consist only of symbolic characters (e.g. <=>, <->, @@, @>, <@, &&, ||, ~)` + (opts?.allowWords?.length ? ` or one of the allowed words: ${opts.allowWords.join(", ")}.` : ".")
1184
+ );
1185
+ }
1186
+ if (op3.includes("--") || op3.includes("/*") || op3.includes("*/")) {
1187
+ throw new Error(
1188
+ `Invalid operator "${op3}" \u2014 must not contain SQL comment sequences (-- /* */).`
1189
+ );
1190
+ }
1191
+ }
1192
+ function compileExpressionIntent(intent, ctx, state) {
1193
+ const kind = intent.kind;
1194
+ switch (kind) {
1195
+ case "customOp": {
1196
+ const i = intent;
1197
+ const operator = i.operator;
1198
+ assertSafeOperator(operator);
1199
+ const leftNode = compileExpressionIntent(i.left, ctx, state);
1200
+ const rightNode = compileExpressionIntent(i.right, ctx, state);
1201
+ return {
1202
+ A_Expr: {
1203
+ kind: "AEXPR_OP",
1204
+ name: [{ String: { sval: operator } }],
1205
+ lexpr: leftNode,
1206
+ rexpr: rightNode
1207
+ }
1208
+ };
1209
+ }
1210
+ case "customFn": {
1211
+ const i = intent;
1212
+ const nameParts = i.name.split(".");
1213
+ const argNodes = i.args.map(
1214
+ (arg) => compileExpressionIntent(arg, ctx, state)
1215
+ );
1216
+ const orderByNodes = i.aggOrderBy && i.aggOrderBy.length > 0 ? i.aggOrderBy.map((ob) => {
1217
+ const colNode = columnRef(
1218
+ ob.field,
1219
+ void 0,
1220
+ void 0,
1221
+ ctx.naming
1236
1222
  );
1223
+ return sortBy(colNode, ob.direction === "desc" ? "DESC" : "ASC");
1224
+ }) : void 0;
1225
+ return funcCall(nameParts, argNodes, {
1226
+ ...orderByNodes ? { orderBy: orderByNodes } : {}
1227
+ });
1228
+ }
1229
+ case "ref": {
1230
+ const i = intent;
1231
+ const dotIdx = i.column.indexOf(".");
1232
+ if (dotIdx !== -1) {
1233
+ const table = i.column.slice(0, dotIdx);
1234
+ const col = i.column.slice(dotIdx + 1);
1235
+ return columnRef(col, table, void 0, ctx.naming);
1237
1236
  }
1237
+ return columnRef(i.column, void 0, void 0, ctx.naming);
1238
1238
  }
1239
- return decisions;
1240
- }
1241
- if ("aggregates" in select) {
1242
- const decisions = [];
1243
- if (select.fields) {
1244
- for (const field of select.fields) {
1245
- decisions.push({ type: "select", column: field, table: rootTable });
1239
+ case "param": {
1240
+ const i = intent;
1241
+ const idx = ++state.paramIndex;
1242
+ state.parameters.push(i.value);
1243
+ return createParamRef(idx);
1244
+ }
1245
+ case "cast": {
1246
+ const i = intent;
1247
+ const typeName = i.typeName;
1248
+ if (typeof typeName !== "string") {
1249
+ throw new Error(
1250
+ `cast(): typeName must be a plain string, got ${typeof typeName}.`
1251
+ );
1246
1252
  }
1253
+ validateTypeName(typeName);
1254
+ const argNode = compileExpressionIntent(i.expr, ctx, state);
1255
+ return typeCast(argNode, typeName);
1247
1256
  }
1248
- for (const agg of select.aggregates) {
1249
- const aggDecision = {
1250
- type: "selectFunction",
1251
- function: agg.function === "count" && agg.field === "*" ? "count" : agg.function,
1252
- column: agg.field ?? "*",
1253
- table: rootTable
1254
- };
1255
- if (agg.as) {
1256
- aggDecision.alias = agg.as;
1257
+ case "literal": {
1258
+ const i = intent;
1259
+ if (i.value === null || i.value === void 0) {
1260
+ return nullConstNode();
1257
1261
  }
1258
- applyFilterCondition(aggDecision, agg.filter, rootTable);
1259
- decisions.push(aggDecision);
1262
+ if (typeof i.value === "boolean") {
1263
+ return booleanConstNode(i.value);
1264
+ }
1265
+ if (typeof i.value === "number") {
1266
+ if (!Number.isFinite(i.value)) {
1267
+ throw new Error(
1268
+ `literal(): numeric value must be finite; got ${i.value}. Use param() for computed values.`
1269
+ );
1270
+ }
1271
+ if (Number.isInteger(i.value)) {
1272
+ return integerNode(i.value);
1273
+ }
1274
+ return floatNode(String(i.value));
1275
+ }
1276
+ if (typeof i.value === "string") {
1277
+ return {
1278
+ A_Const: { sval: { sval: i.value } }
1279
+ };
1280
+ }
1281
+ throw new Error(
1282
+ `literal(): unsupported value type "${typeof i.value}". Only null, boolean, number, and string are allowed. Use param() to bind computed or user-supplied values.`
1283
+ );
1260
1284
  }
1261
- return decisions;
1262
- }
1263
- return [{ type: "select", column: "*", table: rootTable }];
1264
- }
1265
- function convertWhere(where, rootTable) {
1266
- const decision = convertWhereCondition(where, rootTable);
1267
- return decision ? [decision] : [];
1268
- }
1269
- function assertNoUnsupportedSubqueryModifiers(subquery, context) {
1270
- const unsupported = [];
1271
- if (subquery.groupBy && subquery.groupBy.length > 0)
1272
- unsupported.push("GROUP BY");
1273
- if (subquery.having) unsupported.push("HAVING");
1274
- if (subquery.offset != null) unsupported.push("OFFSET");
1275
- if (subquery.distinct) unsupported.push("DISTINCT");
1276
- if (subquery.distinctOn && subquery.distinctOn.length > 0)
1277
- unsupported.push("DISTINCT ON");
1278
- if (subquery.include && subquery.include.length > 0)
1279
- unsupported.push("include (relation hydration)");
1280
- if (subquery.joins && subquery.joins.length > 0) unsupported.push("joins");
1281
- if (subquery.existsWrap) unsupported.push("existsWrap");
1282
- if (subquery.lock) unsupported.push("lock");
1283
- if (subquery.batchValuesSource) unsupported.push("batchValuesSource");
1284
- if (context === "rawExists" || context === "scalar-direct") {
1285
- if (subquery.limit != null) unsupported.push("LIMIT");
1286
- }
1287
- if (context === "rawExists" || context === "scalar-direct") {
1288
- if (subquery.orderBy && subquery.orderBy.length > 0) {
1289
- unsupported.push("ORDER BY");
1285
+ case "unary": {
1286
+ const i = intent;
1287
+ const operator = i.operator;
1288
+ assertSafeOperator(operator, { allowWords: ["NOT"] });
1289
+ const operandNode = compileExpressionIntent(i.operand, ctx, state);
1290
+ return {
1291
+ A_Expr: {
1292
+ kind: "AEXPR_OP",
1293
+ name: [{ String: { sval: operator } }],
1294
+ rexpr: operandNode
1295
+ }
1296
+ };
1290
1297
  }
1291
- } else if (subquery.orderBy && subquery.orderBy.length > 0) {
1292
- const hasExpressionSort = subquery.orderBy.some(
1293
- (o) => !("field" in o) || o.field == null
1294
- );
1295
- if (hasExpressionSort)
1296
- unsupported.push(
1297
- "orderBy with expression (only field-based ORDER BY is supported)"
1298
- );
1299
- }
1300
- if (context === "IN") {
1301
- const select = subquery.select;
1302
- if (!select) {
1303
- unsupported.push(
1304
- "missing select (IN subquery must project exactly one named column)"
1305
- );
1306
- } else if (select.type === "aggregate") {
1307
- unsupported.push(
1308
- "aggregate SELECT (use a scalar subquery comparison instead)"
1309
- );
1310
- } else if (select.type === "expressions") {
1311
- unsupported.push("expressions SELECT (not supported in IN subquery)");
1312
- } else if (select.type === "all") {
1313
- unsupported.push(
1314
- "SELECT * / all (IN subquery must project exactly one named column)"
1315
- );
1316
- } else if (select.type === "fields" || Array.isArray(select.fields) || // Also catch the typeless `{ fields: undefined | null }` shape: the
1317
- // `fields` key is present (triggering isSelectWithFields in the compiler)
1318
- // but the value is not a non-empty array. Without this branch,
1319
- // `{ fields: undefined }` falls through all checks and the compiler
1320
- // silently falls back to SELECT *, producing wrong SQL.
1321
- // Guard: `in` operator crashes on primitives; a string select like 'id'
1322
- // is a valid single-column selector — only check for the `fields` key on
1323
- // actual objects.
1324
- typeof select === "object" && select !== null && "fields" in select) {
1325
- if (!select.fields || select.fields.length === 0) {
1326
- unsupported.push(
1327
- "empty fields list (IN subquery must project exactly one named column)"
1298
+ case "namedArg": {
1299
+ const nae = intent;
1300
+ const name = nae.name;
1301
+ if (typeof name !== "string") {
1302
+ throw new Error(
1303
+ `namedArg(): name must be a plain string, got ${typeof name}.`
1328
1304
  );
1329
- } else if (select.fields.length > 1) {
1330
- unsupported.push(
1331
- `multi-field projection [${select.fields.join(", ")}] (IN subquery must project exactly one named column \u2014 use a single field)`
1305
+ }
1306
+ validateIdentifier(name, "alias");
1307
+ const argNode = compileExpressionIntent(nae.value, ctx, state);
1308
+ return {
1309
+ NamedArgExpr: {
1310
+ arg: argNode,
1311
+ name,
1312
+ argnumber: -1
1313
+ }
1314
+ };
1315
+ }
1316
+ case "star":
1317
+ return {
1318
+ ColumnRef: { fields: [{ A_Star: {} }] }
1319
+ };
1320
+ case "array": {
1321
+ const ae = intent;
1322
+ const elements = ae.elements.map(
1323
+ (el) => compileExpressionIntent(el, ctx, state)
1324
+ );
1325
+ return { A_ArrayExpr: { elements } };
1326
+ }
1327
+ case "subquery": {
1328
+ const sq = intent;
1329
+ if (!ctx.compileSubquery) {
1330
+ throw new Error(
1331
+ "compileExpressionIntent: 'subquery' expression kind requires ctx.compileSubquery to be set. Use asExpr() only in .columns() context, not in standalone expressions."
1332
1332
  );
1333
- } else if (typeof select.fields[0] !== "string") {
1334
- unsupported.push(
1335
- `non-string field element ${JSON.stringify(select.fields[0])} (IN subquery fields must contain a plain column name string)`
1333
+ }
1334
+ const { ast: innerAst, parameters: innerParams } = ctx.compileSubquery(
1335
+ sq.query,
1336
+ state.paramIndex
1337
+ );
1338
+ for (const p of innerParams) {
1339
+ state.parameters.push(p);
1340
+ }
1341
+ state.paramIndex += innerParams.length;
1342
+ return {
1343
+ SubLink: {
1344
+ subLinkType: "EXPR_SUBLINK",
1345
+ subselect: innerAst
1346
+ }
1347
+ };
1348
+ }
1349
+ case "relationColumn": {
1350
+ const rc = intent;
1351
+ const alias = state.aliases.get(rc.relation) ?? rc.relation;
1352
+ return columnRef(rc.column, alias, void 0, ctx.naming);
1353
+ }
1354
+ case "case": {
1355
+ const dispatch = _createWhereDispatcher();
1356
+ const caseIntent = intent;
1357
+ if (!caseIntent.when || caseIntent.when.length === 0) {
1358
+ throw new Error("CASE expression requires at least one WHEN clause");
1359
+ }
1360
+ const caseArgs = caseIntent.when.map((branch) => {
1361
+ const whenNode = dispatch(
1362
+ branch.condition,
1363
+ ctx,
1364
+ state
1336
1365
  );
1366
+ const thenNode = compileExpressionIntent(branch.result, ctx, state);
1367
+ return {
1368
+ CaseWhen: { expr: whenNode, result: thenNode }
1369
+ };
1370
+ });
1371
+ let defresult;
1372
+ if (caseIntent.else !== void 0) {
1373
+ defresult = compileExpressionIntent(caseIntent.else, ctx, state);
1337
1374
  }
1375
+ return {
1376
+ CaseExpr: {
1377
+ args: caseArgs,
1378
+ ...defresult !== void 0 ? { defresult } : {}
1379
+ }
1380
+ };
1338
1381
  }
1339
- }
1340
- const isScalarContext = context === "scalar" || context === "scalar-direct";
1341
- if (isScalarContext) {
1342
- const select = subquery.select;
1343
- if (select?.type === "expressions") {
1344
- unsupported.push("expressions SELECT (not supported in scalar subquery)");
1345
- } else if (select?.type === "fields" && select.fields != null && select.fields.length > 1) {
1346
- unsupported.push(
1347
- `multi-field projection [${select.fields.join(", ")}] (scalar subquery must project exactly one column \u2014 use a single field)`
1348
- );
1349
- } else if (select?.type === "aggregate" && select.aggregates != null && select.aggregates.length > 1) {
1350
- unsupported.push(
1351
- `multi-aggregate projection (scalar subquery must project exactly one column \u2014 use a single aggregate)`
1382
+ default: {
1383
+ throw new Error(
1384
+ `compileExpressionIntent: unsupported expression kind '${kind}'`
1352
1385
  );
1353
1386
  }
1354
1387
  }
1355
- if (unsupported.length > 0) {
1356
- const label = context === "IN" ? "IN" : context === "rawExists" ? "rawExists" : "scalar";
1357
- throw new Error(
1358
- `${label} subquery with ${unsupported.join(", ")} is not supported \u2014 it would silently change which rows match; restructure the query or use a CTE.`
1359
- );
1360
- }
1361
- }
1362
- function isOuterRef(value) {
1363
- if (typeof value !== "object" || value === null) return false;
1364
- const v = value;
1365
- return v.kind === "ref" && v.outer === true;
1366
1388
  }
1367
- function containsOuterRef(where) {
1368
- if (!where || typeof where !== "object") return false;
1369
- const w = where;
1370
- if (isOuterRef(w)) return true;
1371
- for (const value of Object.values(w)) {
1372
- if (Array.isArray(value)) {
1373
- for (const item of value) {
1374
- if (containsOuterRef(item)) return true;
1389
+ var _whereDispatcherFactory, customExpressionHandler;
1390
+ var init_custom = __esm({
1391
+ "src/handlers/expression/custom.ts"() {
1392
+ "use strict";
1393
+ init_ast_helpers();
1394
+ init_param_ref();
1395
+ init_validate();
1396
+ customExpressionHandler = {
1397
+ types: [
1398
+ "customOp",
1399
+ "customFn",
1400
+ "ref",
1401
+ "param",
1402
+ "cast",
1403
+ "unary",
1404
+ "customExpression"
1405
+ ],
1406
+ compile(decision, ctx, state) {
1407
+ const expressionIntent = decision.expressionIntent;
1408
+ return compileExpressionIntent(expressionIntent, ctx, state);
1375
1409
  }
1376
- } else if (typeof value === "object" && value !== null) {
1377
- if (containsOuterRef(value)) return true;
1378
- }
1410
+ };
1379
1411
  }
1380
- return false;
1381
- }
1382
- function convertComparison(cond, rootTable) {
1383
- const rawValue = cond.value;
1384
- const resolvedValue = isOuterRef(rawValue) ? {
1385
- kind: "fieldRef",
1386
- scope: "outer",
1387
- column: rawValue.column
1388
- } : rawValue;
1389
- const result = {
1390
- type: "where",
1391
- column: cond.field,
1392
- operator: cond.operator,
1393
- value: resolvedValue,
1394
- table: rootTable
1395
- };
1396
- if (cond.jsonPath) result.jsonPath = cond.jsonPath;
1397
- if (cond.jsonMode) result.jsonMode = cond.jsonMode;
1398
- return result;
1399
- }
1400
- function convertLike(cond, rootTable) {
1401
- const result = {
1402
- type: "where",
1403
- column: cond.field,
1404
- operator: cond.caseInsensitive ? "ilike" : "like",
1405
- value: cond.pattern,
1412
+ });
1413
+
1414
+ // src/select-expression-handlers.ts
1415
+ function handleColumnExpression(expr, rootTable, decisions) {
1416
+ const decision = {
1417
+ type: "select",
1418
+ column: expr.column,
1406
1419
  table: rootTable
1407
1420
  };
1408
- if (cond.escape !== void 0) result.escape = cond.escape;
1409
- return result;
1421
+ const alias = expr.as ?? expr.alias;
1422
+ if (alias) decision.alias = alias;
1423
+ decisions.push(decision);
1410
1424
  }
1411
- function convertIn(cond, rootTable) {
1412
- const rawSubquery = cond.subquery;
1413
- if (rawSubquery) {
1414
- assertNoUnsupportedSubqueryModifiers(rawSubquery, "IN");
1415
- const selectField = rawSubquery.select;
1416
- const selectColumn = selectField && "fields" in selectField && Array.isArray(selectField.fields) ? selectField.fields[0] ?? "*" : "*";
1417
- const subConditions = rawSubquery.where ? (() => {
1418
- const converted = convertWhereCondition(
1419
- rawSubquery.where,
1420
- rawSubquery.from
1421
- );
1422
- return converted ? [converted] : [];
1423
- })() : [];
1424
- const rawLimit = rawSubquery.limit;
1425
- const rawOrderBy = rawSubquery.orderBy;
1426
- return {
1427
- type: "where",
1428
- column: cond.field,
1429
- operator: cond.not ? "notInSubquery" : "inSubquery",
1430
- targetTable: rawSubquery.from,
1431
- selectColumn,
1432
- conditions: subConditions,
1433
- table: rootTable,
1434
- // Provenance: original QueryIntent for validation in buildPredicateSubquerySelect
1435
- subqueryIntent: rawSubquery,
1436
- ...rawLimit != null && { limit: rawLimit },
1437
- ...rawOrderBy && {
1438
- orderBy: rawOrderBy.map((o) => ({
1439
- field: o.field,
1440
- direction: o.direction?.toLowerCase() ?? "asc"
1441
- }))
1442
- }
1443
- };
1444
- }
1445
- return {
1446
- type: "where",
1447
- column: cond.field,
1448
- operator: cond.not ? "notIn" : "in",
1449
- value: cond.values,
1450
- table: rootTable
1451
- };
1452
- }
1453
- function convertNull(cond, rootTable) {
1454
- return {
1455
- type: "where",
1456
- column: cond.field,
1457
- operator: cond.operator,
1458
- table: rootTable
1459
- };
1460
- }
1461
- function convertRange(cond, rootTable) {
1462
- const rangeOperator = cond.operator;
1463
- const col = cond.field;
1464
- if (rangeOperator === "contains" || rangeOperator === "containedBy" || rangeOperator === "overlaps") {
1465
- return {
1466
- type: "where",
1467
- column: col,
1468
- operator: rangeOperator,
1469
- value: cond.value,
1470
- table: rootTable
1471
- };
1472
- }
1473
- if (rangeOperator === "between") {
1474
- const rangeVal = cond.value;
1475
- return {
1476
- type: "where",
1477
- column: col,
1478
- operator: "between",
1479
- value: [rangeVal.lower, rangeVal.upper],
1480
- table: rootTable
1481
- };
1482
- }
1483
- if (cond.gte !== void 0 && cond.lte !== void 0) {
1484
- return {
1485
- type: "where",
1486
- column: col,
1487
- operator: "between",
1488
- value: [cond.gte, cond.lte],
1489
- table: rootTable
1490
- };
1491
- }
1492
- if (cond.gte !== void 0) {
1493
- return {
1494
- type: "where",
1495
- column: col,
1496
- operator: "gte",
1497
- value: cond.gte,
1498
- table: rootTable
1499
- };
1500
- }
1501
- if (cond.gt !== void 0) {
1502
- return {
1503
- type: "where",
1504
- column: col,
1505
- operator: "gt",
1506
- value: cond.gt,
1425
+ function handleAggregateExpression(expr, rootTable, decisions, applyFilter) {
1426
+ const aggFunc = expr.function;
1427
+ const aggField = expr.field;
1428
+ const aggAs = expr.as;
1429
+ const aggDistinct = expr.distinct;
1430
+ const aggFilter = expr.filter;
1431
+ if (aggFunc === "count" && !aggField) {
1432
+ const decision = {
1433
+ type: "selectFunction",
1434
+ function: "count",
1435
+ column: "*",
1507
1436
  table: rootTable
1508
1437
  };
1509
- }
1510
- if (cond.lte !== void 0) {
1511
- return {
1512
- type: "where",
1513
- column: col,
1514
- operator: "lte",
1515
- value: cond.lte,
1438
+ if (aggAs) decision.alias = aggAs;
1439
+ applyFilter(decision, aggFilter, rootTable);
1440
+ decisions.push(decision);
1441
+ } else if (aggFunc === "count" && aggDistinct && aggField) {
1442
+ const decision = {
1443
+ type: "selectFunction",
1444
+ function: "countDistinct",
1445
+ column: aggField,
1516
1446
  table: rootTable
1517
1447
  };
1518
- }
1519
- if (cond.lt !== void 0) {
1520
- return {
1521
- type: "where",
1522
- column: col,
1523
- operator: "lt",
1524
- value: cond.lt,
1448
+ if (aggAs) decision.alias = aggAs;
1449
+ applyFilter(decision, aggFilter, rootTable);
1450
+ decisions.push(decision);
1451
+ } else {
1452
+ const decision = {
1453
+ type: "selectFunction",
1454
+ function: aggFunc,
1525
1455
  table: rootTable
1526
1456
  };
1457
+ if (aggField) decision.column = aggField;
1458
+ if (aggAs) decision.alias = aggAs;
1459
+ applyFilter(decision, aggFilter, rootTable);
1460
+ decisions.push(decision);
1527
1461
  }
1528
- return null;
1529
1462
  }
1530
- function convertLogicalGroup(cond, rootTable, decisionType) {
1531
- const subDecisions = [];
1532
- for (const sub of cond.conditions) {
1533
- const subDecision = convertWhereCondition(sub, rootTable);
1534
- if (subDecision) subDecisions.push(subDecision);
1535
- }
1536
- if (subDecisions.length === 0) return null;
1537
- return { type: decisionType, conditions: subDecisions };
1463
+ function handleCoalesceExpression(expr, rootTable, decisions) {
1464
+ const decision = {
1465
+ type: "selectFunction",
1466
+ function: "coalesce",
1467
+ args: expr.fields,
1468
+ table: rootTable
1469
+ };
1470
+ if (expr.as) decision.alias = expr.as;
1471
+ decisions.push(decision);
1538
1472
  }
1539
- function convertNot(cond, rootTable) {
1540
- const subDecision = convertWhereCondition(
1541
- cond.condition,
1542
- rootTable
1543
- );
1544
- if (!subDecision) return null;
1545
- return { type: "whereNot", conditions: [subDecision] };
1473
+ function handleRawExpression(expr, rootTable, decisions) {
1474
+ const decision = {
1475
+ type: "selectFunction",
1476
+ function: "raw",
1477
+ args: [expr.sql],
1478
+ table: rootTable
1479
+ };
1480
+ if (expr.as) decision.alias = expr.as;
1481
+ decisions.push(decision);
1546
1482
  }
1547
- function convertExistsLike(cond, operator) {
1548
- const targetTable = cond.relation;
1549
- const subDecisions = cond.where ? convertWhere(cond.where, targetTable) : [];
1550
- const base = { type: "where", operator, targetTable };
1551
- return subDecisions.length > 0 ? { ...base, conditions: subDecisions } : base;
1483
+ function handleWindowExpression(expr, rootTable, decisions) {
1484
+ const windowFunc = expr.function;
1485
+ const windowAlias = expr.alias;
1486
+ const windowField = expr.field;
1487
+ const over = expr.over;
1488
+ const decision = {
1489
+ type: "selectWindow",
1490
+ function: windowFunc,
1491
+ alias: windowAlias,
1492
+ table: rootTable
1493
+ };
1494
+ if (windowField) decision.field = windowField;
1495
+ if (over.partitionBy) decision.partitionBy = over.partitionBy;
1496
+ if (over.orderBy) decision.orderBy = over.orderBy;
1497
+ const windowOffset = expr.offset;
1498
+ const windowDefault = expr.defaultValue;
1499
+ if (windowOffset !== void 0) decision.args = [windowOffset];
1500
+ if (windowDefault !== void 0) decision.value = windowDefault;
1501
+ decisions.push(decision);
1552
1502
  }
1553
- function convertSubquery(cond) {
1554
- const field = cond.field;
1555
- const operator = cond.operator;
1556
- const subquery = cond.subquery;
1557
- if (!subquery || !field) return null;
1558
- assertNoUnsupportedSubqueryModifiers(subquery, "scalar");
1559
- if (subquery.where && containsOuterRef(subquery.where)) {
1560
- throw new Error(
1561
- 'scalar subquery with correlated outerRef() is not yet supported \u2014 use exists("relation", { where: ... }) when a schema relation exists, or restructure the query to avoid the correlation.'
1562
- );
1563
- }
1564
- const targetTable = subquery.from;
1565
- let selectColumn = "*";
1566
- let aggregate;
1567
- const select = subquery.select;
1568
- if (select) {
1569
- if ("type" in select && select.type === "aggregate") {
1570
- const agg = select.aggregates?.[0];
1571
- if (agg) {
1572
- aggregate = agg.function;
1573
- selectColumn = agg.field ?? "*";
1574
- }
1575
- } else if ("fields" in select && select.fields?.length) {
1576
- selectColumn = select.fields[0];
1577
- }
1578
- }
1579
- const subConditions = [];
1580
- if (subquery.where) {
1581
- const innerWhere = convertWhereCondition(
1582
- subquery.where,
1583
- targetTable
1584
- );
1585
- if (innerWhere) subConditions.push(innerWhere);
1503
+ function handleCaseExpression(expr, rootTable, decisions, _applyFilter, convertCondition) {
1504
+ const whenClauses = expr.when;
1505
+ const conditions = whenClauses.map((wc) => ({
1506
+ when: convertCondition(wc.condition, rootTable),
1507
+ // biome-ignore lint/suspicious/noThenProperty: intentional reserved word in decision object
1508
+ then: wc.result
1509
+ }));
1510
+ const decision = {
1511
+ type: "selectExpression",
1512
+ expressionType: "case",
1513
+ table: rootTable
1514
+ };
1515
+ decision.conditions = conditions;
1516
+ if (expr.else) {
1517
+ decision.value = expr.else;
1586
1518
  }
1587
- const rawLimit = subquery.limit;
1588
- const rawOrderBy = subquery.orderBy;
1589
- const opMap = {
1590
- eq: "=",
1591
- neq: "!=",
1592
- gt: ">",
1593
- gte: ">=",
1594
- lt: "<",
1595
- lte: "<="
1519
+ if (expr.as) decision.alias = expr.as;
1520
+ decisions.push(decision);
1521
+ }
1522
+ function handleRelationColumnExpression(expr, rootTable, decisions) {
1523
+ const decision = {
1524
+ type: "selectRelationColumn",
1525
+ relation: expr.relation,
1526
+ column: expr.column ?? "*",
1527
+ table: rootTable
1596
1528
  };
1597
- return {
1598
- type: "where",
1599
- column: field,
1600
- operator: "scalarSubquery",
1601
- targetTable,
1602
- selectColumn,
1603
- subqueryOperator: opMap[operator] ?? "=",
1604
- // Provenance: original QueryIntent for validation in buildPredicateSubquerySelect
1605
- subqueryIntent: subquery,
1606
- ...aggregate && { aggregate },
1607
- ...subConditions.length > 0 && { conditions: subConditions },
1608
- ...rawLimit != null && { limit: rawLimit },
1609
- ...rawOrderBy && rawOrderBy.length > 0 && {
1610
- orderBy: rawOrderBy.map((o) => ({
1611
- field: o.field,
1612
- direction: o.direction?.toLowerCase() ?? "asc"
1613
- }))
1614
- }
1529
+ if (expr.as) decision.alias = expr.as;
1530
+ decisions.push(decision);
1531
+ }
1532
+ function handleArithmeticExpression(expr, rootTable, decisions) {
1533
+ const decision = {
1534
+ type: "selectArithmetic",
1535
+ operator: expr.operator,
1536
+ args: [expr.left, expr.right],
1537
+ table: rootTable
1615
1538
  };
1539
+ if (expr.as) decision.alias = expr.as;
1540
+ decisions.push(decision);
1616
1541
  }
1617
- function convertWhereCondition(condition, rootTable) {
1618
- const cond = condition;
1619
- switch (cond.kind) {
1620
- case "comparison":
1621
- return convertComparison(cond, rootTable);
1622
- case "like":
1623
- return convertLike(cond, rootTable);
1624
- case "in":
1625
- return convertIn(cond, rootTable);
1626
- case "null":
1627
- return convertNull(cond, rootTable);
1628
- case "range":
1629
- return convertRange(cond, rootTable);
1630
- case "and":
1631
- return convertLogicalGroup(cond, rootTable, "whereAnd");
1632
- case "or":
1633
- return convertLogicalGroup(cond, rootTable, "whereOr");
1634
- case "not":
1635
- return convertNot(cond, rootTable);
1636
- case "exists":
1637
- return convertExistsLike(cond, "exists");
1638
- case "notExists":
1639
- return convertExistsLike(cond, "notExists");
1640
- case "relationFilter": {
1641
- const mode = cond.mode || "some";
1642
- const operator = mode === "none" ? "notExists" : mode === "every" ? "every" : "exists";
1643
- return convertExistsLike(cond, operator);
1644
- }
1645
- case "subquery":
1646
- return convertSubquery(cond);
1647
- case "rawExists":
1648
- case "rawNotExists": {
1649
- const sub = cond.subquery;
1650
- if (!sub) {
1651
- throw new Error(
1652
- `${cond.kind}: missing subquery \u2014 pass the result of subquery(table).select(...) or a builder with buildIntent()`
1653
- );
1654
- }
1655
- assertNoUnsupportedSubqueryModifiers(sub, "rawExists");
1656
- if (sub.where && containsOuterRef(sub.where)) {
1657
- throw new Error(
1658
- `${cond.kind}: correlated subqueries (outerRef inside the inner WHERE) are not yet supported. Workaround: use exists("relation", { where: ... }) when a schema relation exists, or wait for the rawExists correlation pipeline (tracked in TODO).`
1659
- );
1660
- }
1661
- return {
1662
- type: "where",
1663
- operator: cond.kind,
1664
- // Reuse expressionIntent (already present on PlanDecision) to carry the
1665
- // inner QueryIntent; the rawExistsHandler discriminates by operator name.
1666
- expressionIntent: sub,
1667
- table: rootTable
1668
- };
1669
- }
1670
- case "jsonContains":
1671
- return {
1672
- type: "where",
1673
- column: cond.field,
1674
- operator: cond.reversed ? "jsonContainedBy" : "jsonContains",
1675
- value: cond.value,
1676
- table: rootTable
1677
- };
1678
- case "any":
1679
- return {
1680
- type: "where",
1681
- column: cond.field,
1682
- operator: "any",
1683
- values: cond.values,
1684
- table: rootTable
1685
- };
1686
- case "jsonExists":
1687
- return {
1688
- type: "where",
1689
- column: cond.field,
1690
- operator: "jsonExists",
1691
- value: cond.key,
1692
- table: rootTable
1693
- };
1694
- case "expression":
1695
- return {
1696
- type: "where",
1697
- operator: "expression",
1698
- expressionIntent: cond.expr,
1699
- value: cond.value,
1700
- subqueryOperator: cond.operator,
1701
- table: rootTable
1702
- };
1703
- default:
1704
- return null;
1705
- }
1542
+ function handleJsonExtractExpression(expr, rootTable, decisions) {
1543
+ const decision = {
1544
+ type: "selectFunction",
1545
+ function: "jsonExtract",
1546
+ column: expr.field,
1547
+ args: expr.path,
1548
+ table: rootTable
1549
+ };
1550
+ if (expr.mode) decision.jsonMode = expr.mode;
1551
+ if (expr.as) decision.alias = expr.as;
1552
+ decisions.push(decision);
1706
1553
  }
1707
- function convertOrderBy(order, rootTable) {
1708
- const direction = order.direction === "desc" ? "DESC" : "ASC";
1709
- const nulls = order.nulls ? order.nulls === "first" ? "FIRST" : "LAST" : void 0;
1710
- if (order.expression) {
1711
- const base = {
1712
- type: "orderBy",
1713
- expressionIntent: order.expression,
1714
- direction,
1715
- table: rootTable
1716
- };
1717
- if (nulls) {
1718
- return { ...base, nulls };
1719
- }
1720
- return base;
1721
- }
1554
+ function handleJsonPathExtractExpression(expr, rootTable, decisions) {
1722
1555
  const decision = {
1723
- type: "orderBy",
1724
- direction,
1725
- table: rootTable,
1726
- // field is optional in OrderByIntent after expression extension (exactOptionalPropertyTypes)
1727
- ...order.field ? { column: order.field } : {}
1556
+ type: "selectFunction",
1557
+ function: "jsonPathExtract",
1558
+ column: expr.field,
1559
+ args: [expr.path],
1560
+ table: rootTable
1728
1561
  };
1729
- if (nulls) {
1730
- return { ...decision, nulls };
1562
+ if (expr.mode) decision.jsonMode = expr.mode;
1563
+ if (expr.as) decision.alias = expr.as;
1564
+ decisions.push(decision);
1565
+ }
1566
+ function handleCustomExpressionSelect(expr, rootTable, decisions) {
1567
+ const decision = {
1568
+ type: "selectCustomExpression",
1569
+ expressionIntent: expr,
1570
+ table: rootTable
1571
+ };
1572
+ const explicitAlias = expr.as;
1573
+ if (explicitAlias) {
1574
+ decision.alias = explicitAlias;
1575
+ } else if (expr.kind === "cast") {
1576
+ const inner = expr.expr;
1577
+ if (inner?.kind === "ref") {
1578
+ const col = inner.column;
1579
+ const dotIdx = col.lastIndexOf(".");
1580
+ decision.alias = dotIdx !== -1 ? col.slice(dotIdx + 1) : col;
1581
+ }
1731
1582
  }
1732
- return decision;
1583
+ decisions.push(decision);
1733
1584
  }
1734
- var init_intent_to_decisions = __esm({
1735
- "src/intent-to-decisions.ts"() {
1585
+ var EXPRESSION_HANDLERS;
1586
+ var init_select_expression_handlers = __esm({
1587
+ "src/select-expression-handlers.ts"() {
1736
1588
  "use strict";
1737
- init_select_expression_handlers();
1589
+ EXPRESSION_HANDLERS = {
1590
+ column: (expr, rootTable, decisions) => handleColumnExpression(expr, rootTable, decisions),
1591
+ columnAlias: (expr, rootTable, decisions) => handleColumnExpression(expr, rootTable, decisions),
1592
+ aggregate: (expr, rootTable, decisions, applyFilter) => handleAggregateExpression(expr, rootTable, decisions, applyFilter),
1593
+ coalesce: (expr, rootTable, decisions) => handleCoalesceExpression(expr, rootTable, decisions),
1594
+ raw: (expr, rootTable, decisions) => handleRawExpression(expr, rootTable, decisions),
1595
+ window: (expr, rootTable, decisions) => handleWindowExpression(expr, rootTable, decisions),
1596
+ case: (expr, rootTable, decisions, applyFilter, convertCondition) => handleCaseExpression(
1597
+ expr,
1598
+ rootTable,
1599
+ decisions,
1600
+ applyFilter,
1601
+ convertCondition
1602
+ ),
1603
+ relationColumn: (expr, rootTable, decisions) => handleRelationColumnExpression(expr, rootTable, decisions),
1604
+ // pseudoColumn: intentional no-op — planner hints, not SQL
1605
+ arithmetic: (expr, rootTable, decisions) => handleArithmeticExpression(expr, rootTable, decisions),
1606
+ jsonExtract: (expr, rootTable, decisions) => handleJsonExtractExpression(expr, rootTable, decisions),
1607
+ jsonPathExtract: (expr, rootTable, decisions) => handleJsonPathExtractExpression(expr, rootTable, decisions),
1608
+ // Custom expression types — produce 'selectCustomExpression' decisions
1609
+ literal: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1610
+ customOp: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1611
+ customFn: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1612
+ ref: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1613
+ param: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1614
+ cast: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions),
1615
+ unary: (expr, rootTable, decisions) => handleCustomExpressionSelect(expr, rootTable, decisions)
1616
+ };
1738
1617
  }
1739
1618
  });
1740
1619
 
1741
- // src/handlers/types.ts
1742
- function createCompilerState() {
1743
- return {
1744
- parameters: [],
1745
- paramIndex: 0,
1746
- ctes: /* @__PURE__ */ new Map(),
1747
- aliases: /* @__PURE__ */ new Map(),
1748
- joins: []
1749
- };
1750
- }
1751
- function isSqlExpression(value) {
1752
- return typeof value === "object" && value !== null && "sql" in value && typeof value.sql === "string";
1620
+ // src/intent-to-decisions.ts
1621
+ function intentToDecisions(intent, rootTable) {
1622
+ const decisions = [];
1623
+ if (intent.select) {
1624
+ decisions.push(...convertSelect(intent.select, rootTable));
1625
+ } else {
1626
+ decisions.push({ type: "select", column: "*", table: rootTable });
1627
+ }
1628
+ if (intent.where) {
1629
+ decisions.push(...convertWhere(intent.where, rootTable));
1630
+ }
1631
+ if (intent.orderBy && intent.orderBy.length > 0) {
1632
+ for (const order of intent.orderBy) {
1633
+ decisions.push(convertOrderBy(order, rootTable));
1634
+ }
1635
+ }
1636
+ if (intent.groupBy && intent.groupBy.length > 0) {
1637
+ for (const col of intent.groupBy) {
1638
+ decisions.push({ type: "groupBy", column: col, table: rootTable });
1639
+ }
1640
+ }
1641
+ if (intent.having) {
1642
+ const havingDecisions = convertWhere(intent.having, rootTable);
1643
+ for (const d of havingDecisions) {
1644
+ decisions.push({ ...d, type: "having" });
1645
+ }
1646
+ }
1647
+ if (intent.distinctOn && intent.distinctOn.length > 0) {
1648
+ decisions.push({ type: "distinctOn", columns: intent.distinctOn });
1649
+ } else if (intent.distinct) {
1650
+ decisions.push({ type: "distinct" });
1651
+ }
1652
+ if (intent.limit !== void 0) {
1653
+ decisions.push({ type: "limit", limit: intent.limit });
1654
+ }
1655
+ if (intent.offset !== void 0) {
1656
+ decisions.push({ type: "offset", offset: intent.offset });
1657
+ }
1658
+ return decisions;
1753
1659
  }
1754
- function isRangeValue(value) {
1755
- return typeof value === "object" && value !== null && ("lower" in value || "upper" in value);
1756
- }
1757
- function isParamRef(value) {
1758
- return typeof value === "object" && value !== null && "paramIndex" in value && typeof value.paramIndex === "number";
1759
- }
1760
- function isSelectWithFields(value) {
1761
- return typeof value === "object" && value !== null && "fields" in value && (Array.isArray(value.fields) || value.fields === void 0);
1660
+ function applyFilterCondition(decision, filter, rootTable) {
1661
+ if (filter) {
1662
+ const filterDecision = convertWhereCondition(filter, rootTable);
1663
+ if (filterDecision) decision.filterCondition = filterDecision;
1664
+ }
1762
1665
  }
1763
- var COMPARISON_OPERATORS, PATTERN_OPERATORS, NULL_OPERATORS, COLLECTION_OPERATORS, LOGICAL_OPERATORS, ALL_OPERATORS;
1764
- var init_types = __esm({
1765
- "src/handlers/types.ts"() {
1766
- "use strict";
1767
- COMPARISON_OPERATORS = {
1768
- EQ: "=",
1769
- NEQ: "!=",
1770
- LT: "<",
1771
- LTE: "<=",
1772
- GT: ">",
1773
- GTE: ">=",
1774
- // Aliases
1775
- eq: "=",
1776
- ne: "!=",
1777
- lt: "<",
1778
- lte: "<=",
1779
- gt: ">",
1780
- gte: ">="
1781
- };
1782
- PATTERN_OPERATORS = {
1783
- LIKE: "like",
1784
- ILIKE: "ilike",
1785
- NOT_LIKE: "notLike",
1786
- NOT_ILIKE: "notIlike"
1787
- };
1788
- NULL_OPERATORS = {
1789
- IS_NULL: "isNull",
1790
- IS_NOT_NULL: "isNotNull"
1791
- };
1792
- COLLECTION_OPERATORS = {
1793
- IN: "in",
1794
- NOT_IN: "notIn",
1795
- ANY: "any"
1796
- };
1797
- LOGICAL_OPERATORS = {
1798
- AND: "and",
1799
- OR: "or",
1800
- NOT: "not"
1801
- };
1802
- ALL_OPERATORS = {
1803
- ...COMPARISON_OPERATORS,
1804
- ...PATTERN_OPERATORS,
1805
- ...NULL_OPERATORS,
1806
- ...COLLECTION_OPERATORS,
1807
- ...LOGICAL_OPERATORS
1808
- };
1666
+ function convertSelect(select, rootTable) {
1667
+ const selectType = "type" in select ? select.type : void 0;
1668
+ if ("all" in select && select.all === true) {
1669
+ return [{ type: "select", column: "*", table: rootTable }];
1809
1670
  }
1810
- });
1811
-
1812
- // src/raw-expression-parser.ts
1813
- function tokenise(input) {
1814
- const tokens = [];
1815
- let i = 0;
1816
- const src = input.trim();
1817
- while (i < src.length) {
1818
- const ch = src[i];
1819
- if (/\s/.test(ch)) {
1820
- i++;
1821
- continue;
1822
- }
1823
- if (/[0-9]/.test(ch)) {
1824
- let num = "";
1825
- while (i < src.length && /[0-9]/.test(src[i])) {
1826
- num += src[i++];
1827
- }
1828
- if (src[i] === "." && i + 1 < src.length && /[0-9]/.test(src[i + 1])) {
1829
- num += src[i++];
1830
- while (i < src.length && /[0-9]/.test(src[i])) {
1831
- num += src[i++];
1832
- }
1833
- tokens.push({ kind: "FLOAT", value: num });
1834
- } else {
1835
- tokens.push({ kind: "INT", value: num });
1671
+ if (selectType === "fields" && "fields" in select) {
1672
+ return select.fields.map((field) => ({
1673
+ type: "select",
1674
+ column: field,
1675
+ table: rootTable
1676
+ }));
1677
+ }
1678
+ if (selectType === "expressions" && "columns" in select) {
1679
+ const decisions = [];
1680
+ const columns = select.columns;
1681
+ for (const exprUnknown of columns) {
1682
+ const expr = exprUnknown;
1683
+ const handler = EXPRESSION_HANDLERS[expr.kind];
1684
+ if (handler) {
1685
+ handler(
1686
+ expr,
1687
+ rootTable,
1688
+ decisions,
1689
+ applyFilterCondition,
1690
+ convertWhereCondition
1691
+ );
1836
1692
  }
1837
- continue;
1838
1693
  }
1839
- if (ch === "'") {
1840
- let str = "";
1841
- i++;
1842
- while (i < src.length) {
1843
- if (src[i] === "'" && src[i + 1] === "'") {
1844
- str += "'";
1845
- i += 2;
1846
- } else if (src[i] === "'") {
1847
- i++;
1848
- break;
1849
- } else {
1850
- str += src[i++];
1851
- }
1694
+ return decisions;
1695
+ }
1696
+ if ("aggregates" in select) {
1697
+ const decisions = [];
1698
+ if (select.fields) {
1699
+ for (const field of select.fields) {
1700
+ decisions.push({ type: "select", column: field, table: rootTable });
1852
1701
  }
1853
- tokens.push({ kind: "STRING", value: str });
1854
- continue;
1855
1702
  }
1856
- if (/[a-zA-Z_]/.test(ch)) {
1857
- let ident = "";
1858
- while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) {
1859
- ident += src[i++];
1703
+ for (const agg of select.aggregates) {
1704
+ const aggDecision = {
1705
+ type: "selectFunction",
1706
+ function: agg.function === "count" && agg.field === "*" ? "count" : agg.function,
1707
+ column: agg.field ?? "*",
1708
+ table: rootTable
1709
+ };
1710
+ if (agg.as) {
1711
+ aggDecision.alias = agg.as;
1860
1712
  }
1861
- tokens.push({ kind: "IDENT", value: ident });
1862
- continue;
1863
- }
1864
- if (ch === ":" && src[i + 1] === ":") {
1865
- tokens.push({ kind: "CAST", value: "::" });
1866
- i += 2;
1867
- continue;
1868
- }
1869
- if (["+", "-", "*", "/"].includes(ch)) {
1870
- tokens.push({ kind: "OP", value: ch });
1871
- i++;
1872
- continue;
1873
- }
1874
- if (ch === "(") {
1875
- tokens.push({ kind: "LPAREN", value: "(" });
1876
- i++;
1877
- continue;
1878
- }
1879
- if (ch === ")") {
1880
- tokens.push({ kind: "RPAREN", value: ")" });
1881
- i++;
1882
- continue;
1883
- }
1884
- if (ch === ",") {
1885
- tokens.push({ kind: "COMMA", value: "," });
1886
- i++;
1887
- continue;
1888
- }
1889
- if (ch === ".") {
1890
- tokens.push({ kind: "DOT", value: "." });
1891
- i++;
1892
- continue;
1713
+ applyFilterCondition(aggDecision, agg.filter, rootTable);
1714
+ decisions.push(aggDecision);
1893
1715
  }
1894
- throw new Error(
1895
- `parseExpression: unexpected character '${ch}' at position ${i} in: ${input}`
1896
- );
1716
+ return decisions;
1897
1717
  }
1898
- tokens.push({ kind: "EOF", value: "" });
1899
- return tokens;
1718
+ return [{ type: "select", column: "*", table: rootTable }];
1900
1719
  }
1901
- function buildAExpr(op3, left, right) {
1902
- return {
1903
- A_Expr: {
1904
- kind: "AEXPR_OP",
1905
- name: [{ String: { sval: op3 } }],
1906
- lexpr: left,
1907
- rexpr: right
1908
- }
1909
- };
1720
+ function convertWhere(where, rootTable) {
1721
+ const decision = convertWhereCondition(where, rootTable);
1722
+ return decision ? [decision] : [];
1910
1723
  }
1911
- function parseExpression(sqlFragment) {
1912
- const trimmed = sqlFragment.trim();
1913
- if (!trimmed) {
1914
- throw new Error("parseExpression: empty SQL fragment");
1724
+ function assertNoUnsupportedSubqueryModifiers(subquery, context) {
1725
+ const unsupported = [];
1726
+ if (subquery.groupBy && subquery.groupBy.length > 0)
1727
+ unsupported.push("GROUP BY");
1728
+ if (subquery.having) unsupported.push("HAVING");
1729
+ if (subquery.offset != null) unsupported.push("OFFSET");
1730
+ if (subquery.distinct) unsupported.push("DISTINCT");
1731
+ if (subquery.distinctOn && subquery.distinctOn.length > 0)
1732
+ unsupported.push("DISTINCT ON");
1733
+ if (subquery.include && subquery.include.length > 0)
1734
+ unsupported.push("include (relation hydration)");
1735
+ if (subquery.joins && subquery.joins.length > 0) unsupported.push("joins");
1736
+ if (subquery.existsWrap) unsupported.push("existsWrap");
1737
+ if (subquery.lock) unsupported.push("lock");
1738
+ if (subquery.batchValuesSource) unsupported.push("batchValuesSource");
1739
+ if (context === "rawExists" || context === "scalar-direct") {
1740
+ if (subquery.limit != null) unsupported.push("LIMIT");
1915
1741
  }
1916
- const tokens = tokenise(trimmed);
1917
- const parser = new Parser(tokens);
1918
- const node = parser.parseExpr();
1919
- if (parser["peek"]().kind !== "EOF") {
1920
- const remaining = tokens.slice(parser["pos"]).map((t) => t.value).join(" ");
1921
- throw new Error(
1922
- `parseExpression: unexpected trailing tokens '${remaining}' in: ${sqlFragment}`
1742
+ if (context === "rawExists" || context === "scalar-direct") {
1743
+ if (subquery.orderBy && subquery.orderBy.length > 0) {
1744
+ unsupported.push("ORDER BY");
1745
+ }
1746
+ } else if (subquery.orderBy && subquery.orderBy.length > 0) {
1747
+ const hasExpressionSort = subquery.orderBy.some(
1748
+ (o) => !("field" in o) || o.field == null
1923
1749
  );
1750
+ if (hasExpressionSort)
1751
+ unsupported.push(
1752
+ "orderBy with expression (only field-based ORDER BY is supported)"
1753
+ );
1924
1754
  }
1925
- return node;
1926
- }
1927
- var Parser;
1928
- var init_raw_expression_parser = __esm({
1929
- "src/raw-expression-parser.ts"() {
1930
- "use strict";
1931
- Parser = class {
1932
- constructor(tokens) {
1933
- this.tokens = tokens;
1934
- }
1935
- tokens;
1936
- pos = 0;
1937
- peek() {
1938
- return this.tokens[this.pos];
1939
- }
1940
- consume() {
1941
- const tok = this.tokens[this.pos];
1942
- this.pos++;
1943
- return tok;
1944
- }
1755
+ if (context === "IN") {
1756
+ const select = subquery.select;
1757
+ if (!select) {
1758
+ unsupported.push(
1759
+ "missing select (IN subquery must project exactly one named column)"
1760
+ );
1761
+ } else if (select.type === "aggregate") {
1762
+ unsupported.push(
1763
+ "aggregate SELECT (use a scalar subquery comparison instead)"
1764
+ );
1765
+ } else if (select.type === "expressions") {
1766
+ unsupported.push("expressions SELECT (not supported in IN subquery)");
1767
+ } else if (select.type === "all") {
1768
+ unsupported.push(
1769
+ "SELECT * / all (IN subquery must project exactly one named column)"
1770
+ );
1771
+ } else if (select.type === "fields" || Array.isArray(select.fields) || // Also catch the typeless `{ fields: undefined | null }` shape: the
1772
+ // `fields` key is present (triggering isSelectWithFields in the compiler)
1773
+ // but the value is not a non-empty array. Without this branch,
1774
+ // `{ fields: undefined }` falls through all checks and the compiler
1775
+ // silently falls back to SELECT *, producing wrong SQL.
1776
+ // Guard: `in` operator crashes on primitives; a string select like 'id'
1777
+ // is a valid single-column selector — only check for the `fields` key on
1778
+ // actual objects.
1779
+ typeof select === "object" && select !== null && "fields" in select) {
1780
+ if (!select.fields || select.fields.length === 0) {
1781
+ unsupported.push(
1782
+ "empty fields list (IN subquery must project exactly one named column)"
1783
+ );
1784
+ } else if (select.fields.length > 1) {
1785
+ unsupported.push(
1786
+ `multi-field projection [${select.fields.join(", ")}] (IN subquery must project exactly one named column \u2014 use a single field)`
1787
+ );
1788
+ } else if (typeof select.fields[0] !== "string") {
1789
+ unsupported.push(
1790
+ `non-string field element ${JSON.stringify(select.fields[0])} (IN subquery fields must contain a plain column name string)`
1791
+ );
1792
+ }
1793
+ }
1794
+ }
1795
+ const isScalarContext = context === "scalar" || context === "scalar-direct";
1796
+ if (isScalarContext) {
1797
+ const select = subquery.select;
1798
+ if (select?.type === "expressions") {
1799
+ unsupported.push("expressions SELECT (not supported in scalar subquery)");
1800
+ } else if (select?.type === "fields" && select.fields != null && select.fields.length > 1) {
1801
+ unsupported.push(
1802
+ `multi-field projection [${select.fields.join(", ")}] (scalar subquery must project exactly one column \u2014 use a single field)`
1803
+ );
1804
+ } else if (select?.type === "aggregate" && select.aggregates != null && select.aggregates.length > 1) {
1805
+ unsupported.push(
1806
+ `multi-aggregate projection (scalar subquery must project exactly one column \u2014 use a single aggregate)`
1807
+ );
1808
+ }
1809
+ }
1810
+ if (unsupported.length > 0) {
1811
+ const label = context === "IN" ? "IN" : context === "rawExists" ? "rawExists" : "scalar";
1812
+ throw new Error(
1813
+ `${label} subquery with ${unsupported.join(", ")} is not supported \u2014 it would silently change which rows match; restructure the query or use a CTE.`
1814
+ );
1815
+ }
1816
+ }
1817
+ function isOuterRef(value) {
1818
+ if (typeof value !== "object" || value === null) return false;
1819
+ const v = value;
1820
+ return v.kind === "ref" && v.outer === true;
1821
+ }
1822
+ function containsOuterRef(where) {
1823
+ if (!where || typeof where !== "object") return false;
1824
+ const w = where;
1825
+ if (isOuterRef(w)) return true;
1826
+ for (const value of Object.values(w)) {
1827
+ if (Array.isArray(value)) {
1828
+ for (const item of value) {
1829
+ if (containsOuterRef(item)) return true;
1830
+ }
1831
+ } else if (typeof value === "object" && value !== null) {
1832
+ if (containsOuterRef(value)) return true;
1833
+ }
1834
+ }
1835
+ return false;
1836
+ }
1837
+ function convertComparison(cond, rootTable) {
1838
+ const rawValue = cond.value;
1839
+ const resolvedValue = isOuterRef(rawValue) ? {
1840
+ kind: "fieldRef",
1841
+ scope: "outer",
1842
+ column: rawValue.column
1843
+ } : rawValue;
1844
+ const result = {
1845
+ type: "where",
1846
+ column: cond.field,
1847
+ operator: cond.operator,
1848
+ value: resolvedValue,
1849
+ table: rootTable
1850
+ };
1851
+ if (cond.jsonPath) result.jsonPath = cond.jsonPath;
1852
+ if (cond.jsonMode) result.jsonMode = cond.jsonMode;
1853
+ return result;
1854
+ }
1855
+ function convertLike(cond, rootTable) {
1856
+ const result = {
1857
+ type: "where",
1858
+ column: cond.field,
1859
+ operator: cond.caseInsensitive ? "ilike" : "like",
1860
+ value: cond.pattern,
1861
+ table: rootTable
1862
+ };
1863
+ if (cond.escape !== void 0) result.escape = cond.escape;
1864
+ return result;
1865
+ }
1866
+ function convertIn(cond, rootTable) {
1867
+ const rawSubquery = cond.subquery;
1868
+ if (rawSubquery) {
1869
+ assertNoUnsupportedSubqueryModifiers(rawSubquery, "IN");
1870
+ const selectField = rawSubquery.select;
1871
+ const selectColumn = selectField && "fields" in selectField && Array.isArray(selectField.fields) ? selectField.fields[0] ?? "*" : "*";
1872
+ const subConditions = rawSubquery.where ? (() => {
1873
+ const converted = convertWhereCondition(
1874
+ rawSubquery.where,
1875
+ rawSubquery.from
1876
+ );
1877
+ return converted ? [converted] : [];
1878
+ })() : [];
1879
+ const rawLimit = rawSubquery.limit;
1880
+ const rawOrderBy = rawSubquery.orderBy;
1881
+ return {
1882
+ type: "where",
1883
+ column: cond.field,
1884
+ operator: cond.not ? "notInSubquery" : "inSubquery",
1885
+ targetTable: rawSubquery.from,
1886
+ selectColumn,
1887
+ conditions: subConditions,
1888
+ table: rootTable,
1889
+ // Provenance: original QueryIntent for validation in buildPredicateSubquerySelect
1890
+ subqueryIntent: rawSubquery,
1891
+ ...rawLimit != null && { limit: rawLimit },
1892
+ ...rawOrderBy && {
1893
+ orderBy: rawOrderBy.map((o) => ({
1894
+ field: o.field,
1895
+ direction: o.direction?.toLowerCase() ?? "asc"
1896
+ }))
1897
+ }
1898
+ };
1899
+ }
1900
+ return {
1901
+ type: "where",
1902
+ column: cond.field,
1903
+ operator: cond.not ? "notIn" : "in",
1904
+ value: cond.values,
1905
+ table: rootTable
1906
+ };
1907
+ }
1908
+ function convertNull(cond, rootTable) {
1909
+ return {
1910
+ type: "where",
1911
+ column: cond.field,
1912
+ operator: cond.operator,
1913
+ table: rootTable
1914
+ };
1915
+ }
1916
+ function convertRange(cond, rootTable) {
1917
+ const rangeOperator = cond.operator;
1918
+ const col = cond.field;
1919
+ if (rangeOperator === "contains" || rangeOperator === "containedBy" || rangeOperator === "overlaps") {
1920
+ return {
1921
+ type: "where",
1922
+ column: col,
1923
+ operator: rangeOperator,
1924
+ value: cond.value,
1925
+ table: rootTable
1926
+ };
1927
+ }
1928
+ if (rangeOperator === "between") {
1929
+ const rangeVal = cond.value;
1930
+ return {
1931
+ type: "where",
1932
+ column: col,
1933
+ operator: "between",
1934
+ value: [rangeVal.lower, rangeVal.upper],
1935
+ table: rootTable
1936
+ };
1937
+ }
1938
+ if (cond.gte !== void 0 && cond.lte !== void 0) {
1939
+ return {
1940
+ type: "where",
1941
+ column: col,
1942
+ operator: "between",
1943
+ value: [cond.gte, cond.lte],
1944
+ table: rootTable
1945
+ };
1946
+ }
1947
+ if (cond.gte !== void 0) {
1948
+ return {
1949
+ type: "where",
1950
+ column: col,
1951
+ operator: "gte",
1952
+ value: cond.gte,
1953
+ table: rootTable
1954
+ };
1955
+ }
1956
+ if (cond.gt !== void 0) {
1957
+ return {
1958
+ type: "where",
1959
+ column: col,
1960
+ operator: "gt",
1961
+ value: cond.gt,
1962
+ table: rootTable
1963
+ };
1964
+ }
1965
+ if (cond.lte !== void 0) {
1966
+ return {
1967
+ type: "where",
1968
+ column: col,
1969
+ operator: "lte",
1970
+ value: cond.lte,
1971
+ table: rootTable
1972
+ };
1973
+ }
1974
+ if (cond.lt !== void 0) {
1975
+ return {
1976
+ type: "where",
1977
+ column: col,
1978
+ operator: "lt",
1979
+ value: cond.lt,
1980
+ table: rootTable
1981
+ };
1982
+ }
1983
+ return null;
1984
+ }
1985
+ function convertLogicalGroup(cond, rootTable, decisionType) {
1986
+ const subDecisions = [];
1987
+ for (const sub of cond.conditions) {
1988
+ const subDecision = convertWhereCondition(sub, rootTable);
1989
+ if (subDecision) subDecisions.push(subDecision);
1990
+ }
1991
+ if (subDecisions.length === 0) return null;
1992
+ return { type: decisionType, conditions: subDecisions };
1993
+ }
1994
+ function convertNot(cond, rootTable) {
1995
+ const subDecision = convertWhereCondition(
1996
+ cond.condition,
1997
+ rootTable
1998
+ );
1999
+ if (!subDecision) return null;
2000
+ return { type: "whereNot", conditions: [subDecision] };
2001
+ }
2002
+ function convertExistsLike(cond, operator) {
2003
+ const targetTable = cond.relation;
2004
+ const subDecisions = cond.where ? convertWhere(cond.where, targetTable) : [];
2005
+ const base = { type: "where", operator, targetTable };
2006
+ return subDecisions.length > 0 ? { ...base, conditions: subDecisions } : base;
2007
+ }
2008
+ function convertSubquery(cond) {
2009
+ const field = cond.field;
2010
+ const operator = cond.operator;
2011
+ const subquery = cond.subquery;
2012
+ if (!subquery || !field) return null;
2013
+ assertNoUnsupportedSubqueryModifiers(subquery, "scalar");
2014
+ if (subquery.where && containsOuterRef(subquery.where)) {
2015
+ throw new Error(
2016
+ 'scalar subquery with correlated outerRef() is not yet supported \u2014 use exists("relation", { where: ... }) when a schema relation exists, or restructure the query to avoid the correlation.'
2017
+ );
2018
+ }
2019
+ const targetTable = subquery.from;
2020
+ let selectColumn = "*";
2021
+ let aggregate;
2022
+ const select = subquery.select;
2023
+ if (select) {
2024
+ if ("type" in select && select.type === "aggregate") {
2025
+ const agg = select.aggregates?.[0];
2026
+ if (agg) {
2027
+ aggregate = agg.function;
2028
+ selectColumn = agg.field ?? "*";
2029
+ }
2030
+ } else if ("fields" in select && select.fields?.length) {
2031
+ selectColumn = select.fields[0];
2032
+ }
2033
+ }
2034
+ const subConditions = [];
2035
+ if (subquery.where) {
2036
+ const innerWhere = convertWhereCondition(
2037
+ subquery.where,
2038
+ targetTable
2039
+ );
2040
+ if (innerWhere) subConditions.push(innerWhere);
2041
+ }
2042
+ const rawLimit = subquery.limit;
2043
+ const rawOrderBy = subquery.orderBy;
2044
+ const opMap = {
2045
+ eq: "=",
2046
+ neq: "!=",
2047
+ gt: ">",
2048
+ gte: ">=",
2049
+ lt: "<",
2050
+ lte: "<="
2051
+ };
2052
+ return {
2053
+ type: "where",
2054
+ column: field,
2055
+ operator: "scalarSubquery",
2056
+ targetTable,
2057
+ selectColumn,
2058
+ subqueryOperator: opMap[operator] ?? "=",
2059
+ // Provenance: original QueryIntent for validation in buildPredicateSubquerySelect
2060
+ subqueryIntent: subquery,
2061
+ ...aggregate && { aggregate },
2062
+ ...subConditions.length > 0 && { conditions: subConditions },
2063
+ ...rawLimit != null && { limit: rawLimit },
2064
+ ...rawOrderBy && rawOrderBy.length > 0 && {
2065
+ orderBy: rawOrderBy.map((o) => ({
2066
+ field: o.field,
2067
+ direction: o.direction?.toLowerCase() ?? "asc"
2068
+ }))
2069
+ }
2070
+ };
2071
+ }
2072
+ function convertWhereCondition(condition, rootTable) {
2073
+ const cond = condition;
2074
+ switch (cond.kind) {
2075
+ case "comparison":
2076
+ return convertComparison(cond, rootTable);
2077
+ case "like":
2078
+ return convertLike(cond, rootTable);
2079
+ case "in":
2080
+ return convertIn(cond, rootTable);
2081
+ case "null":
2082
+ return convertNull(cond, rootTable);
2083
+ case "range":
2084
+ return convertRange(cond, rootTable);
2085
+ case "and":
2086
+ return convertLogicalGroup(cond, rootTable, "whereAnd");
2087
+ case "or":
2088
+ return convertLogicalGroup(cond, rootTable, "whereOr");
2089
+ case "not":
2090
+ return convertNot(cond, rootTable);
2091
+ case "exists":
2092
+ return convertExistsLike(cond, "exists");
2093
+ case "notExists":
2094
+ return convertExistsLike(cond, "notExists");
2095
+ case "relationFilter": {
2096
+ const mode = cond.mode || "some";
2097
+ const operator = mode === "none" ? "notExists" : mode === "every" ? "every" : "exists";
2098
+ return convertExistsLike(cond, operator);
2099
+ }
2100
+ case "subquery":
2101
+ return convertSubquery(cond);
2102
+ case "rawExists":
2103
+ case "rawNotExists": {
2104
+ const sub = cond.subquery;
2105
+ if (!sub) {
2106
+ throw new Error(
2107
+ `${cond.kind}: missing subquery \u2014 pass the result of subquery(table).select(...) or a builder with buildIntent()`
2108
+ );
2109
+ }
2110
+ assertNoUnsupportedSubqueryModifiers(sub, "rawExists");
2111
+ if (sub.where && containsOuterRef(sub.where)) {
2112
+ throw new Error(
2113
+ `${cond.kind}: correlated subqueries (outerRef inside the inner WHERE) are not yet supported. Workaround: use exists("relation", { where: ... }) when a schema relation exists, or wait for the rawExists correlation pipeline (tracked in TODO).`
2114
+ );
2115
+ }
2116
+ return {
2117
+ type: "where",
2118
+ operator: cond.kind,
2119
+ // Reuse expressionIntent (already present on PlanDecision) to carry the
2120
+ // inner QueryIntent; the rawExistsHandler discriminates by operator name.
2121
+ expressionIntent: sub,
2122
+ table: rootTable
2123
+ };
2124
+ }
2125
+ case "jsonContains":
2126
+ return {
2127
+ type: "where",
2128
+ column: cond.field,
2129
+ operator: cond.reversed ? "jsonContainedBy" : "jsonContains",
2130
+ value: cond.value,
2131
+ table: rootTable
2132
+ };
2133
+ case "any":
2134
+ return {
2135
+ type: "where",
2136
+ column: cond.field,
2137
+ operator: "any",
2138
+ values: cond.values,
2139
+ table: rootTable
2140
+ };
2141
+ case "jsonExists":
2142
+ return {
2143
+ type: "where",
2144
+ column: cond.field,
2145
+ operator: "jsonExists",
2146
+ value: cond.key,
2147
+ table: rootTable
2148
+ };
2149
+ case "expression":
2150
+ return {
2151
+ type: "where",
2152
+ operator: "expression",
2153
+ expressionIntent: cond.expr,
2154
+ value: cond.value,
2155
+ subqueryOperator: cond.operator,
2156
+ table: rootTable
2157
+ };
2158
+ default:
2159
+ return null;
2160
+ }
2161
+ }
2162
+ function convertOrderBy(order, rootTable) {
2163
+ const direction = order.direction === "desc" ? "DESC" : "ASC";
2164
+ const nulls = order.nulls ? order.nulls === "first" ? "FIRST" : "LAST" : void 0;
2165
+ if (order.expression) {
2166
+ const base = {
2167
+ type: "orderBy",
2168
+ expressionIntent: order.expression,
2169
+ direction,
2170
+ table: rootTable
2171
+ };
2172
+ if (nulls) {
2173
+ return { ...base, nulls };
2174
+ }
2175
+ return base;
2176
+ }
2177
+ const decision = {
2178
+ type: "orderBy",
2179
+ direction,
2180
+ table: rootTable,
2181
+ // field is optional in OrderByIntent after expression extension (exactOptionalPropertyTypes)
2182
+ ...order.field ? { column: order.field } : {}
2183
+ };
2184
+ if (nulls) {
2185
+ return { ...decision, nulls };
2186
+ }
2187
+ return decision;
2188
+ }
2189
+ var init_intent_to_decisions = __esm({
2190
+ "src/intent-to-decisions.ts"() {
2191
+ "use strict";
2192
+ init_select_expression_handlers();
2193
+ }
2194
+ });
2195
+
2196
+ // src/handlers/types.ts
2197
+ function createCompilerState() {
2198
+ return {
2199
+ parameters: [],
2200
+ paramIndex: 0,
2201
+ ctes: /* @__PURE__ */ new Map(),
2202
+ aliases: /* @__PURE__ */ new Map(),
2203
+ joins: []
2204
+ };
2205
+ }
2206
+ function isSqlExpression(value) {
2207
+ return typeof value === "object" && value !== null && "sql" in value && typeof value.sql === "string";
2208
+ }
2209
+ function isRangeValue(value) {
2210
+ return typeof value === "object" && value !== null && ("lower" in value || "upper" in value);
2211
+ }
2212
+ function isParamRef(value) {
2213
+ return typeof value === "object" && value !== null && "paramIndex" in value && typeof value.paramIndex === "number";
2214
+ }
2215
+ function isSelectWithFields(value) {
2216
+ return typeof value === "object" && value !== null && "fields" in value && (Array.isArray(value.fields) || value.fields === void 0);
2217
+ }
2218
+ var COMPARISON_OPERATORS, PATTERN_OPERATORS, NULL_OPERATORS, COLLECTION_OPERATORS, LOGICAL_OPERATORS, ALL_OPERATORS;
2219
+ var init_types = __esm({
2220
+ "src/handlers/types.ts"() {
2221
+ "use strict";
2222
+ COMPARISON_OPERATORS = {
2223
+ EQ: "=",
2224
+ NEQ: "!=",
2225
+ LT: "<",
2226
+ LTE: "<=",
2227
+ GT: ">",
2228
+ GTE: ">=",
2229
+ // Aliases
2230
+ eq: "=",
2231
+ ne: "!=",
2232
+ lt: "<",
2233
+ lte: "<=",
2234
+ gt: ">",
2235
+ gte: ">="
2236
+ };
2237
+ PATTERN_OPERATORS = {
2238
+ LIKE: "like",
2239
+ ILIKE: "ilike",
2240
+ NOT_LIKE: "notLike",
2241
+ NOT_ILIKE: "notIlike"
2242
+ };
2243
+ NULL_OPERATORS = {
2244
+ IS_NULL: "isNull",
2245
+ IS_NOT_NULL: "isNotNull"
2246
+ };
2247
+ COLLECTION_OPERATORS = {
2248
+ IN: "in",
2249
+ NOT_IN: "notIn",
2250
+ ANY: "any"
2251
+ };
2252
+ LOGICAL_OPERATORS = {
2253
+ AND: "and",
2254
+ OR: "or",
2255
+ NOT: "not"
2256
+ };
2257
+ ALL_OPERATORS = {
2258
+ ...COMPARISON_OPERATORS,
2259
+ ...PATTERN_OPERATORS,
2260
+ ...NULL_OPERATORS,
2261
+ ...COLLECTION_OPERATORS,
2262
+ ...LOGICAL_OPERATORS
2263
+ };
2264
+ }
2265
+ });
2266
+
2267
+ // src/raw-expression-parser.ts
2268
+ function tokenise(input) {
2269
+ const tokens = [];
2270
+ let i = 0;
2271
+ const src = input.trim();
2272
+ while (i < src.length) {
2273
+ const ch = src[i];
2274
+ if (/\s/.test(ch)) {
2275
+ i++;
2276
+ continue;
2277
+ }
2278
+ if (/[0-9]/.test(ch)) {
2279
+ let num = "";
2280
+ while (i < src.length && /[0-9]/.test(src[i])) {
2281
+ num += src[i++];
2282
+ }
2283
+ if (src[i] === "." && i + 1 < src.length && /[0-9]/.test(src[i + 1])) {
2284
+ num += src[i++];
2285
+ while (i < src.length && /[0-9]/.test(src[i])) {
2286
+ num += src[i++];
2287
+ }
2288
+ tokens.push({ kind: "FLOAT", value: num });
2289
+ } else {
2290
+ tokens.push({ kind: "INT", value: num });
2291
+ }
2292
+ continue;
2293
+ }
2294
+ if (ch === "'") {
2295
+ let str = "";
2296
+ i++;
2297
+ while (i < src.length) {
2298
+ if (src[i] === "'" && src[i + 1] === "'") {
2299
+ str += "'";
2300
+ i += 2;
2301
+ } else if (src[i] === "'") {
2302
+ i++;
2303
+ break;
2304
+ } else {
2305
+ str += src[i++];
2306
+ }
2307
+ }
2308
+ tokens.push({ kind: "STRING", value: str });
2309
+ continue;
2310
+ }
2311
+ if (/[a-zA-Z_]/.test(ch)) {
2312
+ let ident = "";
2313
+ while (i < src.length && /[a-zA-Z0-9_]/.test(src[i])) {
2314
+ ident += src[i++];
2315
+ }
2316
+ tokens.push({ kind: "IDENT", value: ident });
2317
+ continue;
2318
+ }
2319
+ if (ch === ":" && src[i + 1] === ":") {
2320
+ tokens.push({ kind: "CAST", value: "::" });
2321
+ i += 2;
2322
+ continue;
2323
+ }
2324
+ if (["+", "-", "*", "/"].includes(ch)) {
2325
+ tokens.push({ kind: "OP", value: ch });
2326
+ i++;
2327
+ continue;
2328
+ }
2329
+ if (ch === "(") {
2330
+ tokens.push({ kind: "LPAREN", value: "(" });
2331
+ i++;
2332
+ continue;
2333
+ }
2334
+ if (ch === ")") {
2335
+ tokens.push({ kind: "RPAREN", value: ")" });
2336
+ i++;
2337
+ continue;
2338
+ }
2339
+ if (ch === ",") {
2340
+ tokens.push({ kind: "COMMA", value: "," });
2341
+ i++;
2342
+ continue;
2343
+ }
2344
+ if (ch === ".") {
2345
+ tokens.push({ kind: "DOT", value: "." });
2346
+ i++;
2347
+ continue;
2348
+ }
2349
+ throw new Error(
2350
+ `parseExpression: unexpected character '${ch}' at position ${i} in: ${input}`
2351
+ );
2352
+ }
2353
+ tokens.push({ kind: "EOF", value: "" });
2354
+ return tokens;
2355
+ }
2356
+ function buildAExpr(op3, left, right) {
2357
+ return {
2358
+ A_Expr: {
2359
+ kind: "AEXPR_OP",
2360
+ name: [{ String: { sval: op3 } }],
2361
+ lexpr: left,
2362
+ rexpr: right
2363
+ }
2364
+ };
2365
+ }
2366
+ function parseExpression(sqlFragment) {
2367
+ const trimmed = sqlFragment.trim();
2368
+ if (!trimmed) {
2369
+ throw new Error("parseExpression: empty SQL fragment");
2370
+ }
2371
+ const tokens = tokenise(trimmed);
2372
+ const parser = new Parser(tokens);
2373
+ const node = parser.parseExpr();
2374
+ if (parser["peek"]().kind !== "EOF") {
2375
+ const remaining = tokens.slice(parser["pos"]).map((t) => t.value).join(" ");
2376
+ throw new Error(
2377
+ `parseExpression: unexpected trailing tokens '${remaining}' in: ${sqlFragment}`
2378
+ );
2379
+ }
2380
+ return node;
2381
+ }
2382
+ var Parser;
2383
+ var init_raw_expression_parser = __esm({
2384
+ "src/raw-expression-parser.ts"() {
2385
+ "use strict";
2386
+ Parser = class {
2387
+ constructor(tokens) {
2388
+ this.tokens = tokens;
2389
+ }
2390
+ tokens;
2391
+ pos = 0;
2392
+ peek() {
2393
+ return this.tokens[this.pos];
2394
+ }
2395
+ consume() {
2396
+ const tok = this.tokens[this.pos];
2397
+ this.pos++;
2398
+ return tok;
2399
+ }
1945
2400
  expect(kind) {
1946
2401
  const tok = this.peek();
1947
2402
  if (tok.kind !== kind) {
@@ -2121,566 +2576,177 @@ var init_raw_expression_parser = __esm({
2121
2576
  int4: "int4",
2122
2577
  int2: "int2",
2123
2578
  smallint: "int2",
2124
- int8: "int8",
2125
- bigint: "int8",
2126
- float4: "float4",
2127
- real: "float4",
2128
- float8: "float8",
2129
- "double precision": "float8",
2130
- bool: "bool",
2131
- boolean: "bool",
2132
- text: "text",
2133
- varchar: "varchar",
2134
- bpchar: "bpchar",
2135
- char: "bpchar",
2136
- bytea: "bytea",
2137
- date: "date",
2138
- time: "time",
2139
- timetz: "timetz",
2140
- timestamp: "timestamp",
2141
- timestamptz: "timestamptz",
2142
- interval: "interval",
2143
- numeric: "numeric",
2144
- decimal: "numeric",
2145
- uuid: "uuid",
2146
- json: "json",
2147
- jsonb: "jsonb",
2148
- oid: "oid",
2149
- name: "name"
2150
- };
2151
- const pgName = pgCatalogTypes[lower];
2152
- if (pgName) {
2153
- return {
2154
- names: [
2155
- { String: { sval: "pg_catalog" } },
2156
- { String: { sval: pgName } }
2157
- ],
2158
- typemod: -1
2159
- };
2160
- }
2161
- return {
2162
- names: [{ String: { sval: lower } }],
2163
- typemod: -1
2164
- };
2165
- }
2166
- };
2167
- }
2168
- });
2169
-
2170
- // src/compiler-utils.ts
2171
- import { InvalidOperationError } from "@dbsp/core";
2172
- function inferPgArrayType(columnName, columnTypes, sampleValue) {
2173
- if (columnTypes?.[columnName]) {
2174
- const pgBase = mapToPgBaseType(columnTypes[columnName]);
2175
- return `${pgBase}[]`;
2176
- }
2177
- if (sampleValue !== null && sampleValue !== void 0) {
2178
- if (typeof sampleValue === "bigint") return "int8[]";
2179
- if (typeof sampleValue === "number") {
2180
- return Number.isInteger(sampleValue) ? "int4[]" : "float8[]";
2181
- }
2182
- if (typeof sampleValue === "string") return "text[]";
2183
- if (typeof sampleValue === "boolean") return "bool[]";
2184
- if (typeof sampleValue === "object") return "jsonb[]";
2185
- }
2186
- return "text[]";
2187
- }
2188
- function mapToPgBaseType(pgType) {
2189
- const normalized = pgType.toUpperCase().replace(/\(.*\)/, "").trim();
2190
- switch (normalized) {
2191
- // ColumnType aliases (lowercase ColumnType values from ModelIR)
2192
- case "STRING":
2193
- return "text";
2194
- case "NUMBER":
2195
- return "float8";
2196
- case "DATETIME":
2197
- return "timestamptz";
2198
- case "TIME":
2199
- return "time";
2200
- // PostgreSQL native types
2201
- case "INTEGER":
2202
- case "INT":
2203
- case "INT4":
2204
- case "SERIAL":
2205
- return "int4";
2206
- case "BIGINT":
2207
- case "INT8":
2208
- case "BIGSERIAL":
2209
- return "int8";
2210
- case "SMALLINT":
2211
- case "INT2":
2212
- return "int2";
2213
- case "REAL":
2214
- case "FLOAT4":
2215
- return "float4";
2216
- case "DOUBLE PRECISION":
2217
- case "FLOAT8":
2218
- case "FLOAT":
2219
- case "NUMERIC":
2220
- case "DECIMAL":
2221
- return "float8";
2222
- case "TEXT":
2223
- case "VARCHAR":
2224
- case "CHAR":
2225
- case "CHARACTER VARYING":
2226
- return "text";
2227
- case "BOOLEAN":
2228
- case "BOOL":
2229
- return "bool";
2230
- case "JSON":
2231
- case "JSONB":
2232
- return "jsonb";
2233
- case "UUID":
2234
- return "uuid";
2235
- case "TIMESTAMP":
2236
- case "TIMESTAMPTZ":
2237
- case "TIMESTAMP WITH TIME ZONE":
2238
- return "timestamptz";
2239
- case "DATE":
2240
- return "date";
2241
- default:
2242
- return pgType.toLowerCase();
2243
- }
2244
- }
2245
- function stripArraySuffix(pgArrayType) {
2246
- return pgArrayType.endsWith("[]") ? pgArrayType.slice(0, -2) : pgArrayType;
2247
- }
2248
- function mapModelIRTypeToPgBase(dataType) {
2249
- const t = dataType.toLowerCase();
2250
- if (t === "integer" || t === "int" || t === "serial" || t === "bigserial")
2251
- return "int4";
2252
- if (t === "bigint") return "int8";
2253
- if (t === "decimal" || t === "float" || t === "double" || t === "real" || t === "numeric")
2254
- return "float8";
2255
- if (t === "text" || t === "string" || t === "varchar" || t === "char")
2256
- return "text";
2257
- if (t === "boolean" || t === "bool") return "bool";
2258
- if (t === "json" || t === "jsonb") return "jsonb";
2259
- if (t === "uuid") return "uuid";
2260
- if (t === "timestamp" || t === "timestamptz" || t === "datetime")
2261
- return "timestamptz";
2262
- if (t === "date") return "date";
2263
- return void 0;
2264
- }
2265
- function transposeToColumnArrays(columns, values) {
2266
- return columns.map((_, colIdx) => values.map((row) => row[colIdx]));
2267
- }
2268
- function validateBatchCardinality(columns, values) {
2269
- for (let i = 0; i < values.length; i++) {
2270
- const row = values[i];
2271
- if (row === void 0 || row.length !== columns.length) {
2272
- throw new InvalidOperationError(
2273
- "insert",
2274
- `Array length mismatch at row ${i}: expected ${columns.length} columns, got ${row?.length ?? 0}`
2275
- );
2276
- }
2277
- }
2278
- }
2279
- function parseRawExpression(sqlFragment) {
2280
- try {
2281
- return parseExpression(sqlFragment);
2282
- } catch {
2283
- throw new Error(
2284
- `sql(): cannot parse raw SQL fragment as expression: ${sqlFragment}`
2285
- );
2286
- }
2287
- }
2288
- var init_compiler_utils = __esm({
2289
- "src/compiler-utils.ts"() {
2290
- "use strict";
2291
- init_raw_expression_parser();
2292
- }
2293
- });
2294
-
2295
- // src/validate.ts
2296
- function validateIdentifier(value, type) {
2297
- if (!value || value.length === 0) {
2298
- throw new InvalidIdentifierError(value, type, "cannot be empty");
2299
- }
2300
- if (value.length > 63) {
2301
- throw new InvalidIdentifierError(
2302
- value,
2303
- type,
2304
- `exceeds maximum length of 63 characters (got ${value.length})`
2305
- );
2306
- }
2307
- const validIdentifierPattern = /^[a-zA-Z_][a-zA-Z0-9_$]*$/;
2308
- if (!validIdentifierPattern.test(value)) {
2309
- if (/[\x00-\x1f\x7f]/.test(value)) {
2310
- throw new InvalidIdentifierError(
2311
- value,
2312
- type,
2313
- "contains control characters"
2314
- );
2315
- }
2316
- if (/^[0-9]/.test(value)) {
2317
- throw new InvalidIdentifierError(
2318
- value,
2319
- type,
2320
- "cannot start with a digit"
2321
- );
2322
- }
2323
- if (/[^\w$]/.test(value)) {
2324
- throw new InvalidIdentifierError(
2325
- value,
2326
- type,
2327
- "contains invalid characters (only letters, digits, underscore, and $ allowed)"
2328
- );
2329
- }
2330
- throw new InvalidIdentifierError(
2331
- value,
2332
- type,
2333
- "does not match valid identifier pattern"
2334
- );
2335
- }
2336
- if (value.includes("\0")) {
2337
- throw new InvalidIdentifierError(value, type, "contains null byte");
2338
- }
2339
- }
2340
- function isReservedKeyword(value) {
2341
- return SQL_RESERVED_KEYWORDS.has(value.toLowerCase());
2342
- }
2343
- function validateQualifiedIdentifier(schemaTable) {
2344
- const parts = schemaTable.split(".");
2345
- if (parts.length === 1) {
2346
- validateIdentifier(parts[0], "table");
2347
- return { table: parts[0] };
2579
+ int8: "int8",
2580
+ bigint: "int8",
2581
+ float4: "float4",
2582
+ real: "float4",
2583
+ float8: "float8",
2584
+ "double precision": "float8",
2585
+ bool: "bool",
2586
+ boolean: "bool",
2587
+ text: "text",
2588
+ varchar: "varchar",
2589
+ bpchar: "bpchar",
2590
+ char: "bpchar",
2591
+ bytea: "bytea",
2592
+ date: "date",
2593
+ time: "time",
2594
+ timetz: "timetz",
2595
+ timestamp: "timestamp",
2596
+ timestamptz: "timestamptz",
2597
+ interval: "interval",
2598
+ numeric: "numeric",
2599
+ decimal: "numeric",
2600
+ uuid: "uuid",
2601
+ json: "json",
2602
+ jsonb: "jsonb",
2603
+ oid: "oid",
2604
+ name: "name"
2605
+ };
2606
+ const pgName = pgCatalogTypes[lower];
2607
+ if (pgName) {
2608
+ return {
2609
+ names: [
2610
+ { String: { sval: "pg_catalog" } },
2611
+ { String: { sval: pgName } }
2612
+ ],
2613
+ typemod: -1
2614
+ };
2615
+ }
2616
+ return {
2617
+ names: [{ String: { sval: lower } }],
2618
+ typemod: -1
2619
+ };
2620
+ }
2621
+ };
2348
2622
  }
2349
- if (parts.length === 2) {
2350
- const schema = parts[0];
2351
- const table = parts[1];
2352
- validateIdentifier(schema, "schema");
2353
- validateIdentifier(table, "table");
2354
- return { schema, table };
2623
+ });
2624
+
2625
+ // src/compiler-utils.ts
2626
+ import { InvalidOperationError, validateTypeName as validateTypeName2 } from "@dbsp/core";
2627
+ function inferPgArrayType(columnName, columnTypes, sampleValue) {
2628
+ if (columnTypes?.[columnName]) {
2629
+ const pgBase = mapToPgBaseType(columnTypes[columnName]);
2630
+ return `${pgBase}[]`;
2355
2631
  }
2356
- throw new InvalidIdentifierError(
2357
- schemaTable,
2358
- "table",
2359
- "too many dots in qualified name (expected schema.table or table)"
2360
- );
2361
- }
2362
- function validateIdentifiers(identifiers) {
2363
- for (const [value, type] of Object.entries(identifiers)) {
2364
- if (value) {
2365
- validateIdentifier(value, type);
2632
+ if (sampleValue !== null && sampleValue !== void 0) {
2633
+ if (typeof sampleValue === "bigint") return "int8[]";
2634
+ if (typeof sampleValue === "number") {
2635
+ return Number.isInteger(sampleValue) ? "int4[]" : "float8[]";
2366
2636
  }
2637
+ if (typeof sampleValue === "string") return "text[]";
2638
+ if (typeof sampleValue === "boolean") return "bool[]";
2639
+ if (typeof sampleValue === "object") return "jsonb[]";
2367
2640
  }
2641
+ return "text[]";
2368
2642
  }
2369
- function sanitizeForDisplay(value) {
2370
- return value.replace(/[\x00-\x1f\x7f]/g, "?").slice(0, 100);
2371
- }
2372
- function validateExtensionName(name, context = "extension") {
2373
- if (!name || name.length === 0) {
2374
- throw new InvalidIdentifierError(name, context, "cannot be empty");
2375
- }
2376
- if (name.length > 63) {
2377
- throw new InvalidIdentifierError(
2378
- name,
2379
- context,
2380
- `exceeds maximum length of 63 characters (got ${name.length})`
2381
- );
2382
- }
2383
- if (/[\x00-\x1f\x7f]/.test(name)) {
2384
- throw new InvalidIdentifierError(
2385
- name,
2386
- context,
2387
- "contains control characters"
2388
- );
2389
- }
2390
- if (/[\\]/.test(name)) {
2391
- throw new InvalidIdentifierError(
2392
- name,
2393
- context,
2394
- "contains backslash (forbidden in extension names)"
2395
- );
2396
- }
2397
- if (/"/.test(name)) {
2398
- throw new InvalidIdentifierError(
2399
- name,
2400
- context,
2401
- "contains double-quote (identifier injection risk)"
2402
- );
2403
- }
2404
- if (/'/.test(name)) {
2405
- throw new InvalidIdentifierError(
2406
- name,
2407
- context,
2408
- "contains single-quote (string injection risk)"
2409
- );
2410
- }
2411
- if (/;/.test(name)) {
2412
- throw new InvalidIdentifierError(
2413
- name,
2414
- context,
2415
- "contains semicolon (statement injection risk)"
2416
- );
2417
- }
2418
- if (/--/.test(name)) {
2419
- throw new InvalidIdentifierError(
2420
- name,
2421
- context,
2422
- "contains line-comment marker (--)"
2423
- );
2424
- }
2425
- if (/\/\*/.test(name)) {
2426
- throw new InvalidIdentifierError(
2427
- name,
2428
- context,
2429
- "contains block-comment opener (/*)"
2430
- );
2431
- }
2432
- if (/\*\//.test(name)) {
2433
- throw new InvalidIdentifierError(
2434
- name,
2435
- context,
2436
- "contains block-comment closer (*/)"
2437
- );
2438
- }
2439
- if (/\$\$/.test(name)) {
2440
- throw new InvalidIdentifierError(
2441
- name,
2442
- context,
2443
- "contains dollar-quoting ($$)"
2444
- );
2445
- }
2446
- if (/\s/.test(name)) {
2447
- throw new InvalidIdentifierError(name, context, "contains whitespace");
2448
- }
2449
- if (!/^[a-zA-Z0-9_\-.]+$/.test(name)) {
2450
- throw new InvalidIdentifierError(
2451
- name,
2452
- context,
2453
- "contains characters not allowed in extension names (only letters, digits, underscore, hyphen, and dot allowed)"
2454
- );
2455
- }
2456
- }
2457
- function validateCollationName(name, context = "collation") {
2458
- if (!name || name.length === 0) {
2459
- throw new InvalidIdentifierError(name, context, "cannot be empty");
2460
- }
2461
- if (Buffer.byteLength(name, "utf8") > 63) {
2462
- throw new InvalidIdentifierError(
2463
- name,
2464
- context,
2465
- `exceeds maximum length of 63 bytes (got ${Buffer.byteLength(name, "utf8")})`
2466
- );
2467
- }
2468
- if (/\x00/.test(name)) {
2469
- throw new InvalidIdentifierError(
2470
- name,
2471
- context,
2472
- "contains NUL byte (\\x00) which would be silently truncated by PostgreSQL"
2473
- );
2474
- }
2475
- if (/[\x01-\x1f\x7f]/.test(name)) {
2476
- throw new InvalidIdentifierError(
2477
- name,
2478
- context,
2479
- "contains control characters (only printable characters allowed)"
2480
- );
2481
- }
2482
- if (/[\\]/.test(name)) {
2483
- throw new InvalidIdentifierError(
2484
- name,
2485
- context,
2486
- "contains backslash (forbidden in collation names)"
2487
- );
2488
- }
2489
- if (/"/.test(name)) {
2490
- throw new InvalidIdentifierError(
2491
- name,
2492
- context,
2493
- "contains double-quote (identifier injection risk)"
2494
- );
2495
- }
2496
- if (/'/.test(name)) {
2497
- throw new InvalidIdentifierError(
2498
- name,
2499
- context,
2500
- "contains single-quote (string injection risk)"
2501
- );
2502
- }
2503
- if (/;/.test(name)) {
2504
- throw new InvalidIdentifierError(
2505
- name,
2506
- context,
2507
- "contains semicolon (statement injection risk)"
2508
- );
2509
- }
2510
- if (/--/.test(name)) {
2511
- throw new InvalidIdentifierError(
2512
- name,
2513
- context,
2514
- "contains line-comment marker (--)"
2515
- );
2516
- }
2517
- if (/\/\*/.test(name)) {
2518
- throw new InvalidIdentifierError(
2519
- name,
2520
- context,
2521
- "contains block-comment opener (/*)"
2522
- );
2523
- }
2524
- if (/\*\//.test(name)) {
2525
- throw new InvalidIdentifierError(
2526
- name,
2527
- context,
2528
- "contains block-comment closer (*/)"
2529
- );
2530
- }
2531
- if (/\$\$/.test(name)) {
2532
- throw new InvalidIdentifierError(
2533
- name,
2534
- context,
2535
- "contains dollar-quoting ($$)"
2536
- );
2537
- }
2538
- if (/\s/.test(name)) {
2539
- throw new InvalidIdentifierError(name, context, "contains whitespace");
2540
- }
2541
- if (!/^[a-zA-Z_][a-zA-Z0-9_.-]*(?:@[A-Za-z0-9-]{1,10})?$/.test(name)) {
2542
- throw new InvalidIdentifierError(
2543
- name,
2544
- context,
2545
- "contains characters not allowed in collation names (only letters, digits, underscore, hyphen, and dot allowed; optional @modifier must be 1-10 alphanumeric/hyphen characters, e.g. @euro, @latin9, @iso8859-15)"
2546
- );
2643
+ function mapToPgBaseType(pgType) {
2644
+ const normalized = pgType.toUpperCase().replace(/\(.*\)/, "").trim();
2645
+ switch (normalized) {
2646
+ // ColumnType aliases (lowercase ColumnType values from ModelIR)
2647
+ case "STRING":
2648
+ return "text";
2649
+ case "NUMBER":
2650
+ return "float8";
2651
+ case "DATETIME":
2652
+ return "timestamptz";
2653
+ case "TIME":
2654
+ return "time";
2655
+ // PostgreSQL native types
2656
+ case "INTEGER":
2657
+ case "INT":
2658
+ case "INT4":
2659
+ case "SERIAL":
2660
+ return "int4";
2661
+ case "BIGINT":
2662
+ case "INT8":
2663
+ case "BIGSERIAL":
2664
+ return "int8";
2665
+ case "SMALLINT":
2666
+ case "INT2":
2667
+ return "int2";
2668
+ case "REAL":
2669
+ case "FLOAT4":
2670
+ return "float4";
2671
+ case "DOUBLE PRECISION":
2672
+ case "FLOAT8":
2673
+ case "FLOAT":
2674
+ case "NUMERIC":
2675
+ case "DECIMAL":
2676
+ return "float8";
2677
+ case "TEXT":
2678
+ case "VARCHAR":
2679
+ case "CHAR":
2680
+ case "CHARACTER VARYING":
2681
+ return "text";
2682
+ case "BOOLEAN":
2683
+ case "BOOL":
2684
+ return "bool";
2685
+ case "JSON":
2686
+ case "JSONB":
2687
+ return "jsonb";
2688
+ case "UUID":
2689
+ return "uuid";
2690
+ case "TIMESTAMP":
2691
+ case "TIMESTAMPTZ":
2692
+ case "TIMESTAMP WITH TIME ZONE":
2693
+ return "timestamptz";
2694
+ case "DATE":
2695
+ return "date";
2696
+ default: {
2697
+ const customType = pgType.toLowerCase();
2698
+ validateTypeName2(customType);
2699
+ return customType;
2700
+ }
2547
2701
  }
2548
2702
  }
2549
- function validateSqlExpression(sql, context) {
2550
- if (/[;]|--|\/\*|\*\/|\$\$|\\/.test(sql)) {
2551
- throw new Error(
2552
- `Unsafe SQL expression in ${context}: contains forbidden characters (;, --, /*, */, "$$" (dollar-quoted strings), \\). Value: "${sql}"`
2553
- );
2703
+ function stripArraySuffix(pgArrayType) {
2704
+ return pgArrayType.endsWith("[]") ? pgArrayType.slice(0, -2) : pgArrayType;
2705
+ }
2706
+ function mapModelIRTypeToPgBase(dataType) {
2707
+ const t = dataType.toLowerCase();
2708
+ if (t === "integer" || t === "int" || t === "serial" || t === "bigserial")
2709
+ return "int4";
2710
+ if (t === "bigint") return "int8";
2711
+ if (t === "decimal" || t === "float" || t === "double" || t === "real" || t === "numeric")
2712
+ return "float8";
2713
+ if (t === "text" || t === "string" || t === "varchar" || t === "char")
2714
+ return "text";
2715
+ if (t === "boolean" || t === "bool") return "bool";
2716
+ if (t === "json" || t === "jsonb") return "jsonb";
2717
+ if (t === "uuid") return "uuid";
2718
+ if (t === "timestamp" || t === "timestamptz" || t === "datetime")
2719
+ return "timestamptz";
2720
+ if (t === "date") return "date";
2721
+ return void 0;
2722
+ }
2723
+ function transposeToColumnArrays(columns, values) {
2724
+ return columns.map((_, colIdx) => values.map((row) => row[colIdx]));
2725
+ }
2726
+ function validateBatchCardinality(columns, values) {
2727
+ for (let i = 0; i < values.length; i++) {
2728
+ const row = values[i];
2729
+ if (row === void 0 || row.length !== columns.length) {
2730
+ throw new InvalidOperationError(
2731
+ "insert",
2732
+ `Array length mismatch at row ${i}: expected ${columns.length} columns, got ${row?.length ?? 0}`
2733
+ );
2734
+ }
2554
2735
  }
2555
2736
  }
2556
- function validateDbTypeName(type) {
2557
- if (!SAFE_TYPE_PATTERN.test(type)) {
2737
+ function parseRawExpression(sqlFragment) {
2738
+ try {
2739
+ return parseExpression(sqlFragment);
2740
+ } catch {
2558
2741
  throw new Error(
2559
- `Unsafe database type name: "${type}". Must match PostgreSQL type name rules.`
2742
+ `sql(): cannot parse raw SQL fragment as expression: ${sqlFragment}`
2560
2743
  );
2561
2744
  }
2562
- return type;
2563
2745
  }
2564
- var SQL_RESERVED_KEYWORDS, InvalidIdentifierError, SAFE_TYPE_PATTERN;
2565
- var init_validate = __esm({
2566
- "src/validate.ts"() {
2746
+ var init_compiler_utils = __esm({
2747
+ "src/compiler-utils.ts"() {
2567
2748
  "use strict";
2568
- SQL_RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
2569
- "all",
2570
- "analyse",
2571
- "analyze",
2572
- "and",
2573
- "any",
2574
- "array",
2575
- "as",
2576
- "asc",
2577
- "asymmetric",
2578
- "authorization",
2579
- "binary",
2580
- "both",
2581
- "case",
2582
- "cast",
2583
- "check",
2584
- "collate",
2585
- "collation",
2586
- "column",
2587
- "concurrently",
2588
- "constraint",
2589
- "create",
2590
- "cross",
2591
- "current_catalog",
2592
- "current_date",
2593
- "current_role",
2594
- "current_schema",
2595
- "current_time",
2596
- "current_timestamp",
2597
- "current_user",
2598
- "default",
2599
- "deferrable",
2600
- "desc",
2601
- "distinct",
2602
- "do",
2603
- "else",
2604
- "end",
2605
- "except",
2606
- "false",
2607
- "fetch",
2608
- "for",
2609
- "foreign",
2610
- "freeze",
2611
- "from",
2612
- "full",
2613
- "grant",
2614
- "group",
2615
- "having",
2616
- "ilike",
2617
- "in",
2618
- "initially",
2619
- "inner",
2620
- "intersect",
2621
- "into",
2622
- "is",
2623
- "isnull",
2624
- "join",
2625
- "lateral",
2626
- "leading",
2627
- "left",
2628
- "like",
2629
- "limit",
2630
- "localtime",
2631
- "localtimestamp",
2632
- "natural",
2633
- "not",
2634
- "notnull",
2635
- "null",
2636
- "offset",
2637
- "on",
2638
- "only",
2639
- "or",
2640
- "order",
2641
- "outer",
2642
- "overlaps",
2643
- "placing",
2644
- "primary",
2645
- "references",
2646
- "returning",
2647
- "right",
2648
- "select",
2649
- "session_user",
2650
- "similar",
2651
- "some",
2652
- "symmetric",
2653
- "table",
2654
- "timestamp",
2655
- "tablesample",
2656
- "then",
2657
- "to",
2658
- "trailing",
2659
- "true",
2660
- "union",
2661
- "unique",
2662
- "user",
2663
- "using",
2664
- "variadic",
2665
- "verbose",
2666
- "when",
2667
- "where",
2668
- "window",
2669
- "with"
2670
- ]);
2671
- InvalidIdentifierError = class extends Error {
2672
- constructor(identifier, identifierType, reason) {
2673
- super(`Invalid ${identifierType} identifier "${identifier}": ${reason}`);
2674
- this.identifier = identifier;
2675
- this.identifierType = identifierType;
2676
- this.reason = reason;
2677
- this.name = "InvalidIdentifierError";
2678
- }
2679
- identifier;
2680
- identifierType;
2681
- reason;
2682
- };
2683
- SAFE_TYPE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/;
2749
+ init_raw_expression_parser();
2684
2750
  }
2685
2751
  });
2686
2752
 
@@ -9516,14 +9582,50 @@ function idxName(table, columns, customName) {
9516
9582
  function buildPolicySQL(tableName, policy, schemaName) {
9517
9583
  const policyName = quoteIdent2(policy.name, "alias");
9518
9584
  const qt = qualifyTable(tableName, schemaName);
9519
- const forClause = policy.command && policy.command !== "ALL" ? ` FOR ${policy.command}` : " FOR ALL";
9585
+ const ALLOWED_RLS_COMMANDS = [
9586
+ "ALL",
9587
+ "SELECT",
9588
+ "INSERT",
9589
+ "UPDATE",
9590
+ "DELETE"
9591
+ ];
9592
+ const rawCommand = policy.command;
9593
+ if (rawCommand !== void 0 && rawCommand !== null && typeof rawCommand !== "string") {
9594
+ throw new Error(
9595
+ `RLS policy command must be a string, got ${typeof rawCommand}.`
9596
+ );
9597
+ }
9598
+ const rlsCommand = rawCommand ? rawCommand.toUpperCase() : "ALL";
9599
+ if (!ALLOWED_RLS_COMMANDS.includes(
9600
+ rlsCommand
9601
+ )) {
9602
+ throw new Error(
9603
+ `Invalid RLS policy command "${rawCommand}". Must be one of: ${ALLOWED_RLS_COMMANDS.join(", ")}.`
9604
+ );
9605
+ }
9606
+ const forClause = rlsCommand !== "ALL" ? ` FOR ${rlsCommand}` : " FOR ALL";
9520
9607
  const asClause = policy.permissive === false ? " AS RESTRICTIVE" : " AS PERMISSIVE";
9521
9608
  const toClause = policy.roles && policy.roles.length > 0 ? ` TO ${policy.roles.map((r) => quoteRoleName(r)).join(", ")}` : "";
9522
- if (policy.using) validateSqlExpression(policy.using, "USING expression");
9523
- if (policy.withCheck)
9524
- validateSqlExpression(policy.withCheck, "WITH CHECK expression");
9525
- const usingClause = policy.using ? ` USING (${policy.using})` : "";
9526
- const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : "";
9609
+ const usingExpr = policy.using;
9610
+ if (usingExpr !== void 0 && usingExpr !== null && usingExpr !== "") {
9611
+ if (typeof usingExpr !== "string") {
9612
+ throw new Error(
9613
+ `RLS policy USING: expression must be a plain string, got ${typeof usingExpr}.`
9614
+ );
9615
+ }
9616
+ validateSqlExpression(usingExpr, "USING expression");
9617
+ }
9618
+ const withCheckExpr = policy.withCheck;
9619
+ if (withCheckExpr !== void 0 && withCheckExpr !== null && withCheckExpr !== "") {
9620
+ if (typeof withCheckExpr !== "string") {
9621
+ throw new Error(
9622
+ `RLS policy WITH CHECK: expression must be a plain string, got ${typeof withCheckExpr}.`
9623
+ );
9624
+ }
9625
+ validateSqlExpression(withCheckExpr, "WITH CHECK expression");
9626
+ }
9627
+ const usingClause = usingExpr ? ` USING (${usingExpr})` : "";
9628
+ const withCheckClause = withCheckExpr ? ` WITH CHECK (${withCheckExpr})` : "";
9527
9629
  return `CREATE POLICY ${policyName} ON ${qt}${forClause}${asClause}${toClause}${usingClause}${withCheckClause};`;
9528
9630
  }
9529
9631
  function buildSequenceClause(verb, seqName, seq, includeCycleNoCycle = false) {
@@ -10257,8 +10359,9 @@ function generateCreateTableSQL(table, schemaName) {
10257
10359
  ${body}
10258
10360
  )`;
10259
10361
  if (table.partition) {
10362
+ const strategy = assertPartitionStrategy(table.partition.strategy);
10260
10363
  const partCols = table.partition.columns.map((n) => quoteIdent2(n, "alias")).join(", ");
10261
- sql += ` PARTITION BY ${table.partition.strategy} (${partCols})`;
10364
+ sql += ` PARTITION BY ${strategy} (${partCols})`;
10262
10365
  }
10263
10366
  sql += ";";
10264
10367
  return sql;
@@ -10300,6 +10403,16 @@ function generateTablesPhase(ctx) {
10300
10403
  }
10301
10404
 
10302
10405
  // src/ddl/ddl-generator.ts
10406
+ var ALLOWED_PARTITION_STRATEGIES = ["RANGE", "LIST", "HASH"];
10407
+ function assertPartitionStrategy(value) {
10408
+ const upper = value.toUpperCase();
10409
+ if (!ALLOWED_PARTITION_STRATEGIES.includes(upper)) {
10410
+ throw new Error(
10411
+ `Invalid partition strategy "${value}". Must be one of: ${ALLOWED_PARTITION_STRATEGIES.join(", ")}.`
10412
+ );
10413
+ }
10414
+ return upper;
10415
+ }
10303
10416
  function generateDDL(schema, options = {}) {
10304
10417
  const {
10305
10418
  includeDropStatements = false,
@@ -10379,8 +10492,9 @@ function generateCreateTable(table, schemaName, naming) {
10379
10492
  ${elementsStr}
10380
10493
  )`;
10381
10494
  if (table.partition) {
10495
+ const strategy = assertPartitionStrategy(table.partition.strategy);
10382
10496
  const partCols = table.partition.columns.map((col) => quoteIdentifier(naming.toDatabase(col))).join(", ");
10383
- sql += ` PARTITION BY ${table.partition.strategy} (${partCols})`;
10497
+ sql += ` PARTITION BY ${strategy} (${partCols})`;
10384
10498
  }
10385
10499
  sql += ";";
10386
10500
  return sql;
@@ -10459,17 +10573,50 @@ function generateCreateIndex(tableName, idx, schemaName, naming) {
10459
10573
  function generateCreatePolicy(tableName, policy, schemaName, naming) {
10460
10574
  const qualifiedTable = qualifyTable2(tableName, schemaName, naming);
10461
10575
  const policyName = quoteIdentifier(policy.name);
10462
- const forClause = policy.command && policy.command !== "ALL" ? ` FOR ${policy.command}` : " FOR ALL";
10576
+ const ALLOWED_RLS_COMMANDS = [
10577
+ "ALL",
10578
+ "SELECT",
10579
+ "INSERT",
10580
+ "UPDATE",
10581
+ "DELETE"
10582
+ ];
10583
+ const rawCommand = policy.command;
10584
+ if (rawCommand !== void 0 && rawCommand !== null && typeof rawCommand !== "string") {
10585
+ throw new Error(
10586
+ `RLS policy command must be a string, got ${typeof rawCommand}.`
10587
+ );
10588
+ }
10589
+ const rlsCommand = rawCommand ? rawCommand.toUpperCase() : "ALL";
10590
+ if (!ALLOWED_RLS_COMMANDS.includes(
10591
+ rlsCommand
10592
+ )) {
10593
+ throw new Error(
10594
+ `Invalid RLS policy command "${rawCommand}". Must be one of: ${ALLOWED_RLS_COMMANDS.join(", ")}.`
10595
+ );
10596
+ }
10597
+ const forClause = rlsCommand !== "ALL" ? ` FOR ${rlsCommand}` : " FOR ALL";
10463
10598
  const asClause = policy.permissive === false ? " AS RESTRICTIVE" : " AS PERMISSIVE";
10464
10599
  const toClause = policy.roles && policy.roles.length > 0 ? ` TO ${policy.roles.map((r) => quoteRoleName(r)).join(", ")}` : "";
10465
- if (policy.using) {
10466
- validateSqlExpression(policy.using, "policy USING expression");
10600
+ const usingExpr = policy.using;
10601
+ if (usingExpr !== void 0 && usingExpr !== null && usingExpr !== "") {
10602
+ if (typeof usingExpr !== "string") {
10603
+ throw new Error(
10604
+ `RLS policy USING: expression must be a plain string, got ${typeof usingExpr}.`
10605
+ );
10606
+ }
10607
+ validateSqlExpression(usingExpr, "policy USING expression");
10467
10608
  }
10468
- if (policy.withCheck) {
10469
- validateSqlExpression(policy.withCheck, "policy WITH CHECK expression");
10609
+ const withCheckExpr = policy.withCheck;
10610
+ if (withCheckExpr !== void 0 && withCheckExpr !== null && withCheckExpr !== "") {
10611
+ if (typeof withCheckExpr !== "string") {
10612
+ throw new Error(
10613
+ `RLS policy WITH CHECK: expression must be a plain string, got ${typeof withCheckExpr}.`
10614
+ );
10615
+ }
10616
+ validateSqlExpression(withCheckExpr, "policy WITH CHECK expression");
10470
10617
  }
10471
- const usingClause = policy.using ? ` USING (${policy.using})` : "";
10472
- const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : "";
10618
+ const usingClause = usingExpr ? ` USING (${usingExpr})` : "";
10619
+ const withCheckClause = withCheckExpr ? ` WITH CHECK (${withCheckExpr})` : "";
10473
10620
  return `CREATE POLICY ${policyName} ON ${qualifiedTable}${forClause}${asClause}${toClause}${usingClause}${withCheckClause};`;
10474
10621
  }
10475
10622
 
@@ -16163,8 +16310,18 @@ function generateAlterColumnSQL(table, column, options, schema) {
16163
16310
  const statements = [];
16164
16311
  if (options.type !== void 0) {
16165
16312
  const safeType = validateDbTypeName(options.type);
16166
- const using = options.using !== void 0 ? ` USING ${options.using}` : "";
16167
- statements.push(`${prefix} TYPE ${safeType}${using}`);
16313
+ const using = options.using;
16314
+ if (using !== void 0) {
16315
+ if (typeof using !== "string") {
16316
+ throw new Error(
16317
+ `ALTER COLUMN USING: expression must be a plain string, got ${typeof using}.`
16318
+ );
16319
+ }
16320
+ validateSqlExpression(using, "ALTER COLUMN USING expression");
16321
+ statements.push(`${prefix} TYPE ${safeType} USING ${using}`);
16322
+ } else {
16323
+ statements.push(`${prefix} TYPE ${safeType}`);
16324
+ }
16168
16325
  }
16169
16326
  if (options.setNotNull === true) {
16170
16327
  statements.push(`${prefix} SET NOT NULL`);