@dbsp/core 1.10.0 → 1.11.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
@@ -58,6 +58,9 @@ __reExport(intent_ast_exports, types_star);
58
58
  import * as types_star from "@dbsp/types";
59
59
 
60
60
  // src/model-impl.ts
61
+ function hasExternalSchema(schemaName) {
62
+ return schemaName != null && schemaName.trim().length > 0;
63
+ }
61
64
  var ModelIRImpl = class {
62
65
  tables;
63
66
  externalTables;
@@ -178,7 +181,7 @@ var ModelIRImpl = class {
178
181
  }
179
182
  for (const table of this.tables.values()) {
180
183
  for (const fk of table.foreignKeys) {
181
- if (!this.tables.has(fk.references.table) && !this.externalTables.has(fk.references.table)) {
184
+ if (!hasExternalSchema(fk.references.schema) && !this.tables.has(fk.references.table) && !this.externalTables.has(fk.references.table)) {
182
185
  errors.push(
183
186
  `Table "${table.name}" has FK referencing non-existent table "${fk.references.table}"`
184
187
  );
@@ -623,11 +626,25 @@ function createTablesProxy(model, tableNames, options) {
623
626
  }
624
627
 
625
628
  // src/dx/schema.ts
629
+ function normalizeReferenceSchema(schemaName) {
630
+ if (schemaName == null) return void 0;
631
+ const t = schemaName.trim();
632
+ return t.length > 0 ? t : void 0;
633
+ }
634
+ function hasExternalSchema2(schemaName) {
635
+ return normalizeReferenceSchema(schemaName) !== void 0;
636
+ }
637
+ function normalizeRefOptions(options) {
638
+ const schemaName = normalizeReferenceSchema(options.schema);
639
+ if (schemaName === options.schema) return options;
640
+ const { schema: _schema, ...rest } = options;
641
+ return schemaName === void 0 ? rest : { ...rest, schema: schemaName };
642
+ }
626
643
  function ref(target, options) {
627
644
  return {
628
645
  __brand: "ref",
629
646
  target,
630
- options: options ?? {}
647
+ options: options ? normalizeRefOptions(options) : {}
631
648
  };
632
649
  }
633
650
  function isRef(value) {
@@ -653,6 +670,7 @@ function schema(definition, constraints, options, extras) {
653
670
  );
654
671
  return {
655
672
  definition,
673
+ ...constraints !== void 0 ? { constraints } : {},
656
674
  model,
657
675
  tableNames,
658
676
  tables,
@@ -721,21 +739,21 @@ function validateRefs(definition, tableNames) {
721
739
  for (const [tableName, tableDef] of Object.entries(definition)) {
722
740
  for (const [columnName, columnDef] of Object.entries(tableDef)) {
723
741
  if (isRef(columnDef)) {
724
- if (!tableSet.has(columnDef.target)) {
742
+ if (!hasExternalSchema2(columnDef.options.schema) && !tableSet.has(columnDef.target)) {
725
743
  throw new SchemaValidationError(
726
744
  `Foreign key '${columnName}' references non-existent table '${columnDef.target}'`,
727
745
  tableName,
728
746
  columnName
729
747
  );
730
748
  }
731
- if (columnDef.target === tableName && !columnDef.options.roles) {
749
+ if (!hasExternalSchema2(columnDef.options.schema) && columnDef.target === tableName && !columnDef.options.roles) {
732
750
  throw new SchemaValidationError(
733
751
  `Self-referential FK '${columnName}' must have 'roles' option with parent/children names`,
734
752
  tableName,
735
753
  columnName
736
754
  );
737
755
  }
738
- if (columnDef.target !== tableName && columnDef.options.roles) {
756
+ if ((hasExternalSchema2(columnDef.options.schema) || columnDef.target !== tableName) && columnDef.options.roles) {
739
757
  throw new SchemaValidationError(
740
758
  `'roles' option is only valid for self-referential FKs, but '${columnName}' references '${columnDef.target}'`,
741
759
  tableName,
@@ -751,14 +769,6 @@ function validateFkTargets(tables) {
751
769
  const tableMap = new Map(tables.map((t) => [t.name, t]));
752
770
  for (const table of tables) {
753
771
  for (const fk of table.foreignKeys) {
754
- const target = tableMap.get(fk.references.table);
755
- if (!target) {
756
- throw new SchemaValidationError(
757
- `Foreign key in '${table.name}' references non-existent table '${fk.references.table}'`,
758
- table.name,
759
- fk.columns[0]
760
- );
761
- }
762
772
  for (const srcCol of fk.columns) {
763
773
  if (!table.columns.some((c) => c.name === srcCol)) {
764
774
  throw new SchemaValidationError(
@@ -782,6 +792,15 @@ function validateFkTargets(tables) {
782
792
  fk.columns[0]
783
793
  );
784
794
  }
795
+ if (hasExternalSchema2(fk.references.schema)) continue;
796
+ const target = tableMap.get(fk.references.table);
797
+ if (!target) {
798
+ throw new SchemaValidationError(
799
+ `Foreign key in '${table.name}' references non-existent table '${fk.references.table}'`,
800
+ table.name,
801
+ fk.columns[0]
802
+ );
803
+ }
785
804
  for (const refCol of fk.references.columns) {
786
805
  if (!target.columns.some((c) => c.name === refCol)) {
787
806
  throw new SchemaValidationError(
@@ -955,6 +974,14 @@ function getReferencedColumnType(targetDef, referencedCol, definition, visited)
955
974
  }
956
975
  function buildRefColumn(columnName, columnDef, definition) {
957
976
  const targetDef = definition[columnDef.target];
977
+ const externalSchema = normalizeReferenceSchema(columnDef.options.schema);
978
+ if (externalSchema !== void 0) {
979
+ throw new SchemaValidationError(
980
+ `Cannot infer column type for foreign key column '${columnName}' to external table '${externalSchema}.${columnDef.target}'. Declare the source column with an explicit type and add the external foreign key at the table level with SchemaConstraints.foreignKeys.`,
981
+ void 0,
982
+ columnName
983
+ );
984
+ }
958
985
  const targetCol = columnDef.options.references?.length === 1 ? columnDef.options.references[0] : void 0;
959
986
  const inferredType = targetCol && targetDef ? getReferencedColumnType(targetDef, targetCol, definition) ?? getTargetPkType(definition, columnDef.target) : getTargetPkType(definition, columnDef.target);
960
987
  const col2 = {
@@ -972,7 +999,8 @@ function buildRefColumn(columnName, columnDef, definition) {
972
999
  columns: [columnName],
973
1000
  references: {
974
1001
  table: columnDef.target,
975
- columns: columnDef.options.references ?? ["id"]
1002
+ columns: columnDef.options.references ?? ["id"],
1003
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {}
976
1004
  }
977
1005
  };
978
1006
  if (columnDef.options.onDelete) fk.onDelete = columnDef.options.onDelete;
@@ -1028,7 +1056,7 @@ function buildPseudoColumns(tableName, refsByTable, finalPk) {
1028
1056
  const refs = refsByTable.get(tableName) ?? [];
1029
1057
  const pkColumn = typeof finalPk === "string" ? finalPk : finalPk?.[0] ?? "id";
1030
1058
  for (const ref3 of refs) {
1031
- if (ref3.options.roles && ref3.target === tableName) {
1059
+ if (!hasExternalSchema2(ref3.options.schema) && ref3.options.roles && ref3.target === tableName) {
1032
1060
  pseudoColumns.push(
1033
1061
  createPseudoColumnMetadata(
1034
1062
  tableName,
@@ -1095,11 +1123,13 @@ function buildTableConstraints(tableName, constraints) {
1095
1123
  tableName
1096
1124
  );
1097
1125
  }
1126
+ const externalSchema = normalizeReferenceSchema(fkRef.options.schema);
1098
1127
  const fk = {
1099
1128
  columns: [...fkRef.options.columns],
1100
1129
  references: {
1101
1130
  table: fkRef.target,
1102
- columns: fkRef.options.references ? [...fkRef.options.references] : ["id"]
1131
+ columns: fkRef.options.references ? [...fkRef.options.references] : ["id"],
1132
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {}
1103
1133
  }
1104
1134
  };
1105
1135
  if (fkRef.options.onDelete) fk.onDelete = fkRef.options.onDelete;
@@ -1147,6 +1177,7 @@ function buildRelations(_definition, refsByTable, tableNames, constraints) {
1147
1177
  for (const tableName of tableNames) {
1148
1178
  const refs = refsByTable.get(tableName) || [];
1149
1179
  for (const ref3 of refs) {
1180
+ if (hasExternalSchema2(ref3.options.schema)) continue;
1150
1181
  if (ref3.options.roles) {
1151
1182
  const roles = ref3.options.roles;
1152
1183
  relations.push({
@@ -1267,6 +1298,7 @@ function addCompositeConstraintRelations(relations, tableNames, constraints) {
1267
1298
  const foreignKeys = constraints[tableName]?.foreignKeys;
1268
1299
  if (!foreignKeys) continue;
1269
1300
  for (const fkRef of foreignKeys) {
1301
+ if (hasExternalSchema2(fkRef.options.schema)) continue;
1270
1302
  if (!tableSet.has(fkRef.target)) continue;
1271
1303
  const columns = fkRef.options.columns;
1272
1304
  if (!columns?.length) continue;
@@ -1338,38 +1370,102 @@ async function getSchemaFromDb(adapter, options) {
1338
1370
  const model = introspectionResult;
1339
1371
  const introspectedAt = introspectionResult.introspectedAt;
1340
1372
  const fkLookup = /* @__PURE__ */ new Map();
1373
+ const compositeExternalFkLookup = /* @__PURE__ */ new Map();
1341
1374
  for (const table of model.tables.values()) {
1342
1375
  const tableFks = /* @__PURE__ */ new Map();
1343
1376
  for (const fk of table.foreignKeys) {
1377
+ const externalSchema = normalizeReferenceSchema(fk.references.schema);
1378
+ if (externalSchema !== void 0 && fk.columns.length > 1) {
1379
+ const tableCompositeFks = compositeExternalFkLookup.get(table.name) ?? [];
1380
+ tableCompositeFks.push({
1381
+ ...fk,
1382
+ references: {
1383
+ ...fk.references,
1384
+ schema: externalSchema
1385
+ }
1386
+ });
1387
+ compositeExternalFkLookup.set(table.name, tableCompositeFks);
1388
+ continue;
1389
+ }
1344
1390
  const fkColumn = fk.columns[0];
1345
1391
  if (!fkColumn) continue;
1346
1392
  const column = table.columns.find((c) => c.name === fkColumn);
1347
1393
  tableFks.set(fkColumn, {
1348
1394
  target: fk.references.table,
1395
+ refs: fk.references.columns,
1396
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {},
1349
1397
  nullable: column?.nullable ?? true,
1350
- unique: column?.unique ?? false
1398
+ unique: column?.unique ?? false,
1399
+ ...fk.onDelete !== void 0 ? { onDelete: fk.onDelete } : {},
1400
+ ...fk.onUpdate !== void 0 ? { onUpdate: fk.onUpdate } : {}
1351
1401
  });
1352
1402
  }
1353
1403
  fkLookup.set(table.name, tableFks);
1354
1404
  }
1355
1405
  const definition = {};
1406
+ const constraints = {};
1356
1407
  for (const table of model.tables.values()) {
1357
1408
  const tableDef = {};
1358
1409
  const tableFks = fkLookup.get(table.name) ?? /* @__PURE__ */ new Map();
1410
+ const compositeExternalFks = compositeExternalFkLookup.get(table.name) ?? [];
1359
1411
  for (const column of table.columns) {
1360
1412
  const fk = tableFks.get(column.name);
1361
1413
  if (fk) {
1414
+ if (fk.schema !== void 0) {
1415
+ const sourceColumn = {
1416
+ type: columnTypeToJsType(column.type),
1417
+ nullable: fk.nullable
1418
+ };
1419
+ if (fk.unique) sourceColumn.unique = true;
1420
+ tableDef[column.name] = sourceColumn;
1421
+ constraints[table.name] ??= {};
1422
+ const tableConstraints = constraints[table.name];
1423
+ tableConstraints.foreignKeys ??= [];
1424
+ const foreignKeys = tableConstraints.foreignKeys;
1425
+ foreignKeys.push(
1426
+ ref(fk.target, {
1427
+ schema: fk.schema,
1428
+ columns: [column.name],
1429
+ references: fk.refs,
1430
+ ...fk.onDelete !== void 0 ? { onDelete: fk.onDelete } : {},
1431
+ ...fk.onUpdate !== void 0 ? { onUpdate: fk.onUpdate } : {}
1432
+ })
1433
+ );
1434
+ continue;
1435
+ }
1436
+ const usesDefaultReferences = fk.refs.length === 1 && fk.refs[0] === "id";
1362
1437
  tableDef[column.name] = ref(fk.target, {
1363
1438
  nullable: fk.nullable,
1364
- unique: fk.unique
1439
+ unique: fk.unique,
1440
+ ...!usesDefaultReferences ? { references: fk.refs } : {},
1441
+ ...fk.onDelete !== void 0 && fk.onDelete !== "NO ACTION" ? { onDelete: fk.onDelete } : {},
1442
+ ...fk.onUpdate !== void 0 && fk.onUpdate !== "NO ACTION" ? { onUpdate: fk.onUpdate } : {}
1365
1443
  });
1366
1444
  } else {
1367
1445
  tableDef[column.name] = columnTypeToJsType(column.type);
1368
1446
  }
1369
1447
  }
1448
+ for (const fk of compositeExternalFks) {
1449
+ const externalSchema = fk.references.schema;
1450
+ if (externalSchema === void 0) continue;
1451
+ constraints[table.name] ??= {};
1452
+ const tableConstraints = constraints[table.name];
1453
+ tableConstraints.foreignKeys ??= [];
1454
+ const foreignKeys = tableConstraints.foreignKeys;
1455
+ foreignKeys.push(
1456
+ ref(fk.references.table, {
1457
+ schema: externalSchema,
1458
+ columns: fk.columns,
1459
+ references: fk.references.columns,
1460
+ ...fk.onDelete !== void 0 ? { onDelete: fk.onDelete } : {},
1461
+ ...fk.onUpdate !== void 0 ? { onUpdate: fk.onUpdate } : {}
1462
+ })
1463
+ );
1464
+ }
1370
1465
  definition[table.name] = tableDef;
1371
1466
  }
1372
1467
  const tableNames = Object.keys(definition);
1468
+ const generatedConstraints = Object.keys(constraints).length > 0 ? constraints : void 0;
1373
1469
  const tables = createTablesProxy(
1374
1470
  model,
1375
1471
  tableNames
@@ -1380,6 +1476,9 @@ async function getSchemaFromDb(adapter, options) {
1380
1476
  tableNames,
1381
1477
  tables
1382
1478
  };
1479
+ if (generatedConstraints !== void 0) {
1480
+ result.constraints = generatedConstraints;
1481
+ }
1383
1482
  if (adapter.dbCasing !== void 0) {
1384
1483
  result.dbCasing = adapter.dbCasing;
1385
1484
  }
@@ -1464,7 +1563,7 @@ function detectForeignKeys(tableName, table, conventions, tableNames) {
1464
1563
  for (const [colName, colDef] of Object.entries(table)) {
1465
1564
  if (colDef.references) {
1466
1565
  const targetTable = colDef.references.table;
1467
- if (tableNames.has(targetTable)) {
1566
+ if (colDef.references.schema === void 0 && tableNames.has(targetTable)) {
1468
1567
  const inferredName = colName.endsWith("Id") ? colName.slice(0, -2) : singularize(targetTable);
1469
1568
  const isSelfRef = targetTable === tableName;
1470
1569
  const detectedFK = {
@@ -3219,367 +3318,780 @@ function batchValues(data, columns, types, opts) {
3219
3318
  });
3220
3319
  }
3221
3320
 
3222
- // src/dx/expressions.ts
3223
- var OPERATOR_PATTERN = /^[a-zA-Z_<>=!@#%^&|~*+\-/.]+$/;
3224
- var FUNCTION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
3225
- var TYPE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\[\])?$/;
3226
- function toExpressionIntent(input) {
3227
- if (input instanceof ExpressionRef) return input.intent;
3228
- if (typeof input === "object" && input !== null && "__expr" in input && input.__expr === true && "intent" in input) {
3229
- return input.intent;
3321
+ // src/dx/column-utils.ts
3322
+ function getColumnName(field) {
3323
+ if (typeof field === "string") {
3324
+ return field;
3230
3325
  }
3231
- if (typeof input === "string") {
3232
- return { kind: "ref", column: input };
3326
+ const colName = field[COLUMN_META];
3327
+ if (colName === void 0) {
3328
+ throw new Error("Invalid ColumnRef: missing COLUMN_META");
3233
3329
  }
3234
- return { kind: "param", value: input };
3330
+ return colName;
3235
3331
  }
3236
- var ExpressionRef = class _ExpressionRef {
3237
- __expr = true;
3238
- intent;
3239
- constructor(intent) {
3240
- this.intent = intent;
3332
+
3333
+ // src/dx/window-functions.ts
3334
+ var WindowBuilder = class _WindowBuilder {
3335
+ constructor(fnKind, partitions = [], orders = []) {
3336
+ this.fnKind = fnKind;
3337
+ this.partitions = partitions;
3338
+ this.orders = orders;
3241
3339
  }
3340
+ fnKind;
3341
+ partitions;
3342
+ orders;
3242
3343
  /**
3243
- * Set an alias for this expression in SELECT.
3244
- * Returns a new ExpressionRef — does not mutate.
3344
+ * Create a ranking window builder (row_number, rank, dense_rank)
3345
+ * @internal Use factory functions instead: rowNumber(), rank(), denseRank()
3245
3346
  */
3246
- as(alias) {
3247
- return new _ExpressionRef({
3248
- ...this.intent,
3249
- as: alias
3250
- });
3251
- }
3252
- /** WHERE: expr = value */
3253
- eq(value) {
3254
- return { kind: "expression", expr: this.intent, operator: "eq", value };
3255
- }
3256
- /** WHERE: expr != value */
3257
- neq(value) {
3258
- return { kind: "expression", expr: this.intent, operator: "neq", value };
3347
+ static ranking(fn2) {
3348
+ return new _WindowBuilder({ type: "ranking", fn: fn2 });
3259
3349
  }
3260
- /** WHERE: expr > value */
3261
- gt(value) {
3262
- return { kind: "expression", expr: this.intent, operator: "gt", value };
3350
+ /**
3351
+ * Create an aggregate window builder (sum, avg, count, min, max)
3352
+ * @internal Use factory functions instead: sum(field), avg(field), etc.
3353
+ */
3354
+ static aggregate(fn2, field) {
3355
+ return new _WindowBuilder({
3356
+ type: "aggregate",
3357
+ fn: fn2,
3358
+ ...field !== void 0 ? { field } : {}
3359
+ });
3263
3360
  }
3264
- /** WHERE: expr >= value */
3265
- gte(value) {
3266
- return { kind: "expression", expr: this.intent, operator: "gte", value };
3361
+ /**
3362
+ * Create an offset window builder (lag, lead)
3363
+ * @internal Use factory functions instead: lag(field), lead(field)
3364
+ */
3365
+ static offset(fn2, field) {
3366
+ return new _WindowBuilder({ type: "offset", fn: fn2, field });
3267
3367
  }
3268
- /** WHERE: expr < value */
3269
- lt(value) {
3270
- return { kind: "expression", expr: this.intent, operator: "lt", value };
3368
+ /**
3369
+ * Add partition field(s) to the OVER clause.
3370
+ * Multiple calls APPEND fields (not replace).
3371
+ * Supports both string field names and ColumnRef (DX-040).
3372
+ *
3373
+ * @example
3374
+ * sum('amount').partitionBy('user_id').partitionBy('category')
3375
+ * // → PARTITION BY "user_id", "category"
3376
+ *
3377
+ * @example DX-040 with ColumnRef
3378
+ * rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
3379
+ */
3380
+ partitionBy(...fields) {
3381
+ const fieldNames = fields.map(
3382
+ (f) => typeof f === "string" ? f : getColumnName(f)
3383
+ );
3384
+ return new _WindowBuilder(
3385
+ this.fnKind,
3386
+ [...this.partitions, ...fieldNames],
3387
+ this.orders
3388
+ );
3271
3389
  }
3272
- /** WHERE: expr <= value */
3273
- lte(value) {
3274
- return { kind: "expression", expr: this.intent, operator: "lte", value };
3390
+ /**
3391
+ * Add order field to the OVER clause.
3392
+ * Multiple calls APPEND fields (not replace).
3393
+ * Supports both string field names and ColumnRef (DX-040).
3394
+ *
3395
+ * @param field - Column name or ColumnRef to order by
3396
+ * @param direction - Sort direction: 'asc' (default) or 'desc'
3397
+ *
3398
+ * @example
3399
+ * rowNumber().orderBy('created_at').orderBy('id', 'desc')
3400
+ * // → ORDER BY "created_at" ASC, "id" DESC
3401
+ *
3402
+ * @example DX-040 with ColumnRef
3403
+ * rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
3404
+ */
3405
+ orderBy(field, direction = "asc") {
3406
+ const fieldName = typeof field === "string" ? field : getColumnName(field);
3407
+ return new _WindowBuilder(this.fnKind, this.partitions, [
3408
+ ...this.orders,
3409
+ { field: fieldName, direction }
3410
+ ]);
3275
3411
  }
3276
3412
  /**
3277
- * Add a FILTER (WHERE ...) clause to this function expression.
3278
- * Only valid on `fn()` expressions (customFn kind).
3279
- * Returns a new ExpressionRef — does not mutate.
3413
+ * Finalize the window expression with an alias.
3414
+ * Returns ExpressionSpec for use in columns().
3280
3415
  *
3281
- * @example fn('array_agg', ref('name')).filter(eq('active', true))
3282
- * → array_agg("name") FILTER (WHERE "active" = $1)
3283
- * @throws Error if called on non-customFn expressions
3416
+ * @param alias - Required alias for the result column
3417
+ *
3418
+ * @example
3419
+ * columns(['id', rowNumber().orderBy('date').as('rn')])
3284
3420
  */
3285
- filter(condition) {
3286
- if (this.intent.kind !== "customFn") {
3287
- throw new Error(
3288
- `filter() can only be used on function expressions created with fn(). Got kind: '${this.intent.kind}'`
3289
- );
3421
+ as(alias) {
3422
+ return {
3423
+ __expr: true,
3424
+ intent: this.toWindowIntent(alias)
3425
+ };
3426
+ }
3427
+ /**
3428
+ * Convert builder state to WindowIntent — produces the correct discriminated branch.
3429
+ * - ranking → RankingWindowIntent (no field)
3430
+ * - aggregate → AggregateWindowIntent (field required; COUNT uses '*' when omitted)
3431
+ * - offset → OffsetWindowIntent (field required)
3432
+ */
3433
+ toWindowIntent(alias) {
3434
+ const over = {};
3435
+ if (this.partitions.length > 0) {
3436
+ over.partitionBy = this.partitions;
3290
3437
  }
3291
- return new _ExpressionRef({
3292
- ...this.intent,
3293
- filter: condition
3294
- });
3438
+ if (this.orders.length > 0) {
3439
+ over.orderBy = this.orders;
3440
+ }
3441
+ if (this.fnKind.type === "ranking") {
3442
+ return {
3443
+ kind: "window",
3444
+ function: this.fnKind.fn,
3445
+ alias,
3446
+ over
3447
+ };
3448
+ }
3449
+ if (this.fnKind.type === "offset") {
3450
+ return {
3451
+ kind: "window",
3452
+ function: this.fnKind.fn,
3453
+ field: this.fnKind.field,
3454
+ alias,
3455
+ over
3456
+ };
3457
+ }
3458
+ const { field } = this.fnKind;
3459
+ return {
3460
+ kind: "window",
3461
+ function: this.fnKind.fn,
3462
+ ...field !== void 0 && { field },
3463
+ alias,
3464
+ over
3465
+ };
3295
3466
  }
3296
3467
  };
3297
- function ref2(column) {
3298
- return new ExpressionRef({
3299
- kind: "ref",
3300
- column
3301
- });
3468
+ function rowNumber() {
3469
+ return WindowBuilder.ranking("row_number");
3302
3470
  }
3303
- function param(value) {
3304
- return new ExpressionRef({
3305
- kind: "param",
3306
- value
3307
- });
3471
+ function rank() {
3472
+ return WindowBuilder.ranking("rank");
3308
3473
  }
3309
- function cast(expr, typeName) {
3310
- if (!TYPE_NAME_PATTERN.test(typeName)) {
3311
- throw new Error(`Invalid type name: ${typeName}`);
3312
- }
3313
- return new ExpressionRef({
3314
- kind: "cast",
3315
- expr: expr.intent,
3316
- typeName
3317
- });
3474
+ function denseRank() {
3475
+ return WindowBuilder.ranking("dense_rank");
3318
3476
  }
3319
- function op(operator, left, right) {
3320
- if (!operator || !OPERATOR_PATTERN.test(operator)) {
3321
- throw new Error(`Invalid operator: ${operator}`);
3322
- }
3323
- return new ExpressionRef({
3324
- kind: "customOp",
3325
- operator,
3326
- left: toExpressionIntent(left),
3327
- right: toExpressionIntent(right)
3328
- });
3477
+ function wSum(field) {
3478
+ return WindowBuilder.aggregate("sum", field);
3329
3479
  }
3330
- function fn(name, ...args) {
3331
- if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
3332
- throw new Error(`Invalid function name: ${name}`);
3333
- }
3334
- const regularArgs = [];
3335
- const orderByArgs = [];
3336
- for (const arg of args) {
3337
- if (isAggOrderByArg(arg)) {
3338
- orderByArgs.push(arg);
3339
- } else {
3340
- regularArgs.push(toExpressionIntent(arg));
3341
- }
3342
- }
3343
- const intent = {
3344
- kind: "customFn",
3345
- name,
3346
- args: regularArgs,
3347
- ...orderByArgs.length > 0 ? { aggOrderBy: orderByArgs } : {}
3348
- };
3349
- return new ExpressionRef(intent);
3480
+ function wAvg(field) {
3481
+ return WindowBuilder.aggregate("avg", field);
3350
3482
  }
3351
- function literal(value) {
3352
- return new ExpressionRef({
3353
- kind: "literal",
3354
- value
3355
- });
3483
+ function wCount(field) {
3484
+ return WindowBuilder.aggregate("count", field);
3356
3485
  }
3357
- function unary(operator, expr) {
3358
- if (!operator || !OPERATOR_PATTERN.test(operator)) {
3359
- throw new Error(`Invalid operator: ${operator}`);
3360
- }
3361
- return new ExpressionRef({
3362
- kind: "unary",
3486
+ function wMin(field) {
3487
+ return WindowBuilder.aggregate("min", field);
3488
+ }
3489
+ function wMax(field) {
3490
+ return WindowBuilder.aggregate("max", field);
3491
+ }
3492
+ function lag(field) {
3493
+ return WindowBuilder.offset("lag", field);
3494
+ }
3495
+ function lead(field) {
3496
+ return WindowBuilder.offset("lead", field);
3497
+ }
3498
+
3499
+ // src/dx/filters.ts
3500
+ function distinct(field) {
3501
+ return { field, distinct: true };
3502
+ }
3503
+ function isDistinctField(value) {
3504
+ return typeof value === "object" && value !== null && "field" in value && typeof value.field === "string" && "distinct" in value && value.distinct === true;
3505
+ }
3506
+ function createComparisonFilter(operator) {
3507
+ return (field, value) => ({
3508
+ kind: "comparison",
3509
+ field: getColumnName(field),
3363
3510
  operator,
3364
- operand: toExpressionIntent(expr)
3511
+ value
3365
3512
  });
3366
3513
  }
3367
- function namedArg(name, value) {
3368
- if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
3369
- throw new Error(`namedArg: invalid argument name: ${name}`);
3514
+ var eq = createComparisonFilter("eq");
3515
+ var neq = createComparisonFilter("neq");
3516
+ var gt = createComparisonFilter("gt");
3517
+ var gte = createComparisonFilter("gte");
3518
+ var lt = createComparisonFilter("lt");
3519
+ var lte = createComparisonFilter("lte");
3520
+ var isDistinctFrom = createComparisonFilter("isDistinctFrom");
3521
+ function like(field, pattern, options) {
3522
+ const caseInsensitive = typeof options === "boolean" ? options : options?.caseInsensitive;
3523
+ const escapeChar = typeof options === "object" ? options.escape : void 0;
3524
+ const intent = {
3525
+ kind: "like",
3526
+ field: getColumnName(field),
3527
+ pattern
3528
+ };
3529
+ const withCi = caseInsensitive !== void 0 ? { ...intent, caseInsensitive } : intent;
3530
+ if (escapeChar !== void 0) {
3531
+ return { ...withCi, escape: escapeChar };
3370
3532
  }
3371
- return new ExpressionRef({
3372
- kind: "namedArg",
3373
- name,
3374
- value: toExpressionIntent(value)
3375
- });
3533
+ return withCi;
3376
3534
  }
3377
- function star() {
3378
- return new ExpressionRef({ kind: "star" });
3535
+ function inArray(field, values) {
3536
+ return { kind: "in", field: getColumnName(field), values };
3379
3537
  }
3380
- function array(...items) {
3381
- return new ExpressionRef({
3382
- kind: "array",
3383
- elements: items.map(toExpressionIntent)
3384
- });
3538
+ function inSubquery(field, query) {
3539
+ const expr = "build" in query ? query.build() : query;
3540
+ return {
3541
+ kind: "in",
3542
+ field: getColumnName(field),
3543
+ subquery: expr.toIntent()
3544
+ };
3385
3545
  }
3386
- function isAggOrderByArg(input) {
3387
- return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof ExpressionRef) && "__aggOrderBy" in input && input.__aggOrderBy === true;
3546
+ function any(field, values) {
3547
+ return { kind: "any", field: getColumnName(field), values };
3388
3548
  }
3389
- function aggOrderBy(field, direction = "asc") {
3390
- return { __aggOrderBy: true, field, direction };
3549
+ function isNull(field) {
3550
+ return { kind: "null", field: getColumnName(field), operator: "isNull" };
3391
3551
  }
3392
- function arrayAgg(col2, ...rest) {
3393
- const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3394
- return fn("array_agg", colExpr, ...rest);
3552
+ function isNotNull(field) {
3553
+ return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
3395
3554
  }
3396
- function stringAgg(col2, separator, ...rest) {
3397
- const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3398
- return fn("string_agg", colExpr, separator, ...rest);
3555
+ function and(...conditions) {
3556
+ const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
3557
+ return { kind: "and", conditions: flatConditions };
3399
3558
  }
3400
-
3401
- // src/dx/case-when-builder.ts
3402
- function toResultIntent(value) {
3403
- if (value instanceof ExpressionRef) {
3404
- return value.intent;
3559
+ function or(...conditions) {
3560
+ const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
3561
+ return { kind: "or", conditions: flatConditions };
3562
+ }
3563
+ function not(condition) {
3564
+ return { kind: "not", condition };
3565
+ }
3566
+ function exists(relation, options) {
3567
+ const result = { kind: "exists", relation };
3568
+ if (options?.where !== void 0) {
3569
+ result.where = options.where;
3405
3570
  }
3406
- if (value === null || value === void 0) {
3407
- return { kind: "literal", value: null };
3571
+ if (options?.recursive !== void 0) {
3572
+ result.recursive = options.recursive;
3408
3573
  }
3409
- if (typeof value === "string") {
3410
- return { kind: "ref", column: value };
3574
+ if (options?.include !== void 0) {
3575
+ result.include = options.include;
3411
3576
  }
3412
- return { kind: "literal", value };
3577
+ return result;
3413
3578
  }
3414
- var CaseBuilder = class _CaseBuilder {
3415
- branches;
3416
- constructor(branches) {
3417
- this.branches = branches;
3579
+ function notExists(relation, options) {
3580
+ const result = {
3581
+ kind: "notExists",
3582
+ relation
3583
+ };
3584
+ if (options?.where !== void 0) {
3585
+ result.where = options.where;
3418
3586
  }
3419
- when(condition, thenValue) {
3420
- return new _CaseBuilder([
3421
- ...this.branches,
3422
- { condition, result: toResultIntent(thenValue) }
3423
- ]);
3587
+ if (options?.recursive !== void 0) {
3588
+ result.recursive = options.recursive;
3424
3589
  }
3425
- else(elseValue) {
3426
- const intent = {
3427
- kind: "case",
3428
- when: this.branches,
3429
- else: toResultIntent(elseValue)
3430
- };
3431
- return new ExpressionRef(intent);
3590
+ if (options?.include !== void 0) {
3591
+ result.include = options.include;
3432
3592
  }
3433
- as(alias) {
3434
- return this.toExpr().as(alias);
3593
+ return result;
3594
+ }
3595
+ function rawExists(sq) {
3596
+ const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
3597
+ return { kind: "rawExists", subquery: intent };
3598
+ }
3599
+ function rawNotExists(sq) {
3600
+ const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
3601
+ return { kind: "rawNotExists", subquery: intent };
3602
+ }
3603
+ function getRelationName(rel) {
3604
+ const meta = rel[RELATION_META];
3605
+ if (!meta) {
3606
+ throw new Error("Invalid RelationRef: missing RELATION_META");
3435
3607
  }
3436
- toExpr() {
3437
- const intent = {
3438
- kind: "case",
3439
- when: this.branches
3440
- };
3441
- return new ExpressionRef(intent);
3608
+ return meta.target;
3609
+ }
3610
+ function every(relation, filter) {
3611
+ const relationName = getRelationName(relation);
3612
+ const where = filter(relation);
3613
+ return {
3614
+ kind: "relationFilter",
3615
+ relation: relationName,
3616
+ where,
3617
+ mode: "every"
3618
+ };
3619
+ }
3620
+ function none(relation, filter) {
3621
+ const relationName = getRelationName(relation);
3622
+ const where = filter(relation);
3623
+ return {
3624
+ kind: "relationFilter",
3625
+ relation: relationName,
3626
+ where,
3627
+ mode: "none"
3628
+ };
3629
+ }
3630
+ function some(relation, filter) {
3631
+ const relationName = getRelationName(relation);
3632
+ const where = filter(relation);
3633
+ return {
3634
+ kind: "relationFilter",
3635
+ relation: relationName,
3636
+ where,
3637
+ mode: "some"
3638
+ };
3639
+ }
3640
+ function coalesce(fields, as) {
3641
+ if (fields.length === 0) {
3642
+ throw new Error("coalesce() requires at least one field");
3442
3643
  }
3443
- };
3444
- function caseWhen(condition, thenValue) {
3445
- return new CaseBuilder([{ condition, result: toResultIntent(thenValue) }]);
3644
+ if (!as || as.trim() === "") {
3645
+ throw new Error("coalesce() requires a non-empty alias");
3646
+ }
3647
+ for (const f of fields) {
3648
+ validateIdentifier(f, "column");
3649
+ }
3650
+ validateIdentifier(as, "column");
3651
+ return {
3652
+ __expr: true,
3653
+ intent: { kind: "coalesce", fields, as }
3654
+ };
3446
3655
  }
3447
-
3448
- // src/dx/builder-utils.ts
3449
- function requireAdapter(adapter, operationName) {
3450
- if (!adapter) {
3656
+ function raw(sqlFragment, as) {
3657
+ if (typeof as !== "string" || as.trim().length === 0) {
3451
3658
  throw new InvalidOperationError(
3452
- operationName,
3453
- "This operation requires an adapter \u2014 pass an adapter when creating the ORM"
3659
+ "raw",
3660
+ "raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
3454
3661
  );
3455
3662
  }
3456
- return adapter;
3457
- }
3663
+ validateIdentifier(as, "column");
3664
+ return {
3665
+ __expr: true,
3666
+ intent: { kind: "raw", sql: sqlFragment, as }
3667
+ };
3668
+ }
3669
+ function col(column, alias) {
3670
+ validateIdentifier(column, "column");
3671
+ validateIdentifier(alias, "column");
3672
+ return {
3673
+ __expr: true,
3674
+ intent: { kind: "columnAlias", column, alias }
3675
+ };
3676
+ }
3677
+ function relationColumn(relation, column, as) {
3678
+ if (!relation) {
3679
+ throw new Error("relationColumn() requires a non-empty relation path");
3680
+ }
3681
+ for (const segment of relation.split(".")) {
3682
+ validateIdentifier(segment, "relation");
3683
+ }
3684
+ if (column !== "*") {
3685
+ validateIdentifier(column, "column");
3686
+ }
3687
+ validateIdentifier(as, "column");
3688
+ return {
3689
+ __expr: true,
3690
+ intent: { kind: "relationColumn", relation, column, as }
3691
+ };
3692
+ }
3693
+ var SQL_RAW_MARKER = /* @__PURE__ */ Symbol.for("dbsp:raw-sql");
3694
+ function sql(sqlFragment) {
3695
+ if (!sqlFragment || sqlFragment.trim() === "") {
3696
+ throw new Error("sql() requires a non-empty SQL fragment");
3697
+ }
3698
+ return { [SQL_RAW_MARKER]: true, sql: sqlFragment };
3699
+ }
3700
+ function isSqlRaw(value) {
3701
+ return typeof value === "object" && value !== null && SQL_RAW_MARKER in value && value[SQL_RAW_MARKER] === true;
3702
+ }
3458
3703
 
3459
- // src/dx/cte-builder.ts
3460
- var CteBuilder = class {
3461
- cteName;
3462
- adapter;
3463
- schemaName;
3464
- unnestColumns;
3465
- indexColumnName;
3466
- constructor(name, adapter, schemaName) {
3467
- this.cteName = name;
3468
- this.adapter = adapter;
3469
- this.schemaName = schemaName;
3704
+ // src/dx/expressions.ts
3705
+ var OPERATOR_PATTERN = /^[a-zA-Z_<>=!@#%^&|~*+\-/.]+$/;
3706
+ var FUNCTION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
3707
+ var TYPE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\[\])?$/;
3708
+ function toExpressionIntent(input) {
3709
+ if (input instanceof ExpressionRef) return input.intent;
3710
+ if (typeof input === "object" && input !== null && "__expr" in input && input.__expr === true && "intent" in input) {
3711
+ return input.intent;
3470
3712
  }
3471
- /**
3472
- * Provide the column arrays for the unnest() CTE.
3473
- * All arrays must have the same length.
3474
- *
3475
- * @param columns - A record of column name → array of values.
3476
- */
3477
- fromUnnest(columns) {
3478
- const lengths = Object.values(columns).map((arr) => arr.length);
3479
- if (lengths.length > 1 && !lengths.every((l) => l === lengths[0])) {
3480
- throw new InvalidOperationError(
3481
- "withCte",
3482
- `Array length mismatch in CTE unnest columns: lengths are [${lengths.join(", ")}]`
3483
- );
3484
- }
3485
- this.unnestColumns = columns;
3486
- return this;
3713
+ if (typeof input === "string") {
3714
+ return { kind: "ref", column: input };
3715
+ }
3716
+ return { kind: "param", value: input };
3717
+ }
3718
+ var ExpressionRef = class _ExpressionRef {
3719
+ __expr = true;
3720
+ intent;
3721
+ constructor(intent) {
3722
+ this.intent = intent;
3487
3723
  }
3488
3724
  /**
3489
- * Add a 0-based ordinality index column (uses WITH ORDINALITY).
3490
- *
3491
- * @param columnName - The name of the index column in the CTE.
3725
+ * Set an alias for this expression in SELECT.
3726
+ * Returns a new ExpressionRef — does not mutate.
3492
3727
  */
3493
- withIndex(columnName) {
3494
- this.indexColumnName = columnName;
3495
- return this;
3728
+ as(alias) {
3729
+ return new _ExpressionRef({
3730
+ ...this.intent,
3731
+ as: alias
3732
+ });
3733
+ }
3734
+ /** WHERE: expr = value */
3735
+ eq(value) {
3736
+ return { kind: "expression", expr: this.intent, operator: "eq", value };
3737
+ }
3738
+ /** WHERE: expr != value */
3739
+ neq(value) {
3740
+ return { kind: "expression", expr: this.intent, operator: "neq", value };
3741
+ }
3742
+ /** WHERE: expr > value */
3743
+ gt(value) {
3744
+ return { kind: "expression", expr: this.intent, operator: "gt", value };
3745
+ }
3746
+ /** WHERE: expr >= value */
3747
+ gte(value) {
3748
+ return { kind: "expression", expr: this.intent, operator: "gte", value };
3749
+ }
3750
+ /** WHERE: expr < value */
3751
+ lt(value) {
3752
+ return { kind: "expression", expr: this.intent, operator: "lt", value };
3753
+ }
3754
+ /** WHERE: expr <= value */
3755
+ lte(value) {
3756
+ return { kind: "expression", expr: this.intent, operator: "lte", value };
3496
3757
  }
3497
3758
  /**
3498
- * Attach an outer query and produce a CteQueryBuilder for execution.
3759
+ * Add a FILTER (WHERE ...) clause to this function expression.
3760
+ * Valid on `fn()` expressions (customFn kind), including DISTINCT calls
3761
+ * produced by `fn(aggName, distinct(field))` (still customFn kind).
3762
+ * Returns a new ExpressionRef — does not mutate.
3499
3763
  *
3500
- * @param selectBuilder - A QueryBuilder (from orm.select(...))
3764
+ * NOTE: the FILTER clause is only emitted when the expression is used in
3765
+ * column position (`.columns([...])`) — the adapter patches it in at that
3766
+ * one SELECT-list branch (compiler.ts). Using `.filter()` inside
3767
+ * `.orderBy()`, `.having()`, `cast()`, or any other nested context compiles
3768
+ * successfully but silently drops the FILTER clause (tracked separately,
3769
+ * not specific to DISTINCT).
3770
+ *
3771
+ * @example fn('array_agg', ref('name')).filter(eq('active', true))
3772
+ * → array_agg("name") FILTER (WHERE "active" = $1) // in .columns()
3773
+ * @example fn('count', distinct('id')).filter(eq('active', true))
3774
+ * → count(DISTINCT id) FILTER (WHERE "active" = $1) // in .columns()
3775
+ * @throws Error if called on any other expression kind
3501
3776
  */
3502
- query(selectBuilder) {
3503
- if (!this.unnestColumns) {
3504
- throw new InvalidOperationError(
3505
- "withCte",
3506
- "CTE requires a data source \u2014 call .fromUnnest() first"
3777
+ filter(condition) {
3778
+ if (this.intent.kind !== "customFn") {
3779
+ throw new Error(
3780
+ `filter() can only be used on function expressions created with fn(). Got kind: '${this.intent.kind}'`
3507
3781
  );
3508
3782
  }
3509
- const cteIntent = {
3510
- kind: "unnestCte",
3511
- name: this.cteName,
3512
- columns: this.unnestColumns,
3513
- ...this.indexColumnName !== void 0 && {
3514
- indexColumn: this.indexColumnName
3515
- }
3516
- };
3517
- return new CteQueryBuilder(
3518
- cteIntent,
3519
- selectBuilder,
3520
- this.adapter,
3521
- this.schemaName
3522
- );
3783
+ return new _ExpressionRef({
3784
+ ...this.intent,
3785
+ filter: condition
3786
+ });
3523
3787
  }
3524
3788
  };
3525
- var CteQueryBuilder = class {
3526
- cteIntent;
3527
- outerBuilder;
3528
- adapter;
3529
- schemaName;
3530
- constructor(cteIntent, outerBuilder, adapter, schemaName) {
3531
- this.cteIntent = cteIntent;
3532
- this.outerBuilder = outerBuilder;
3533
- this.adapter = adapter;
3534
- this.schemaName = schemaName;
3535
- }
3536
- /**
3537
- * Build the CteQueryIntent AST.
3538
- */
3539
- buildIntent() {
3540
- const queryIntent = this.outerBuilder.buildIntent();
3541
- return {
3542
- kind: "cteQuery",
3543
- ctes: [this.cteIntent],
3544
- query: queryIntent
3545
- };
3789
+ function ref2(column) {
3790
+ return new ExpressionRef({
3791
+ kind: "ref",
3792
+ column
3793
+ });
3794
+ }
3795
+ function param(value) {
3796
+ return new ExpressionRef({
3797
+ kind: "param",
3798
+ value
3799
+ });
3800
+ }
3801
+ function cast(expr, typeName) {
3802
+ if (!TYPE_NAME_PATTERN.test(typeName)) {
3803
+ throw new Error(`Invalid type name: ${typeName}`);
3546
3804
  }
3547
- requireAdapter() {
3548
- return requireAdapter(this.adapter, "withCte");
3805
+ return new ExpressionRef({
3806
+ kind: "cast",
3807
+ expr: expr.intent,
3808
+ typeName
3809
+ });
3810
+ }
3811
+ function op(operator, left, right) {
3812
+ if (!operator || !OPERATOR_PATTERN.test(operator)) {
3813
+ throw new Error(`Invalid operator: ${operator}`);
3549
3814
  }
3550
- /**
3551
- * Compile to SQL and return an observability dump.
3552
- */
3553
- dump() {
3554
- const adapter = this.requireAdapter();
3555
- const intent = this.buildIntent();
3556
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
3557
- const compiled = adapter.compileCteQuery(
3558
- intent,
3559
- compileOptions
3560
- );
3561
- return {
3562
- sql: compiled.sql,
3563
- params: compiled.parameters,
3564
- intent
3565
- };
3815
+ return new ExpressionRef({
3816
+ kind: "customOp",
3817
+ operator,
3818
+ left: toExpressionIntent(left),
3819
+ right: toExpressionIntent(right)
3820
+ });
3821
+ }
3822
+ function fn(name, ...args) {
3823
+ if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
3824
+ throw new Error(`Invalid function name: ${name}`);
3566
3825
  }
3567
- /**
3568
- * Execute the CTE query and return all results.
3569
- */
3570
- async all() {
3571
- const adapter = this.requireAdapter();
3572
- const intent = this.buildIntent();
3573
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
3574
- const compiled = adapter.compileCteQuery(
3575
- intent,
3576
- compileOptions
3577
- );
3578
- return adapter.execute(compiled);
3826
+ const regularArgs = [];
3827
+ const orderByArgs = [];
3828
+ let distinct2 = false;
3829
+ for (let i = 0; i < args.length; i++) {
3830
+ const arg = args[i];
3831
+ if (isDistinctField2(arg)) {
3832
+ if (i !== 0) {
3833
+ throw new Error(
3834
+ `fn('${name}', ...): distinct() is only supported as the first argument \u2014 SQL DISTINCT applies to the whole argument list, not a single position.`
3835
+ );
3836
+ }
3837
+ const field = arg.field;
3838
+ if (field === "*") {
3839
+ throw new Error(
3840
+ `fn('${name}', distinct('*')): DISTINCT on '*' is not valid SQL \u2014 '*' has no columns to deduplicate on. Provide a specific column, or omit distinct() (e.g. fn('${name}', star())) for a plain star call.`
3841
+ );
3842
+ }
3843
+ distinct2 = true;
3844
+ regularArgs.push(toExpressionIntent(field));
3845
+ } else if (isAggOrderByArg(arg)) {
3846
+ orderByArgs.push(arg);
3847
+ } else {
3848
+ regularArgs.push(toExpressionIntent(arg));
3849
+ }
3579
3850
  }
3580
- /**
3581
- * Alias for all().
3582
- */
3851
+ const intent = {
3852
+ kind: "customFn",
3853
+ name,
3854
+ args: regularArgs,
3855
+ ...distinct2 ? { distinct: true } : {},
3856
+ ...orderByArgs.length > 0 ? { aggOrderBy: orderByArgs } : {}
3857
+ };
3858
+ return new ExpressionRef(intent);
3859
+ }
3860
+ function literal(value) {
3861
+ return new ExpressionRef({
3862
+ kind: "literal",
3863
+ value
3864
+ });
3865
+ }
3866
+ function unary(operator, expr) {
3867
+ if (!operator || !OPERATOR_PATTERN.test(operator)) {
3868
+ throw new Error(`Invalid operator: ${operator}`);
3869
+ }
3870
+ return new ExpressionRef({
3871
+ kind: "unary",
3872
+ operator,
3873
+ operand: toExpressionIntent(expr)
3874
+ });
3875
+ }
3876
+ function namedArg(name, value) {
3877
+ if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
3878
+ throw new Error(`namedArg: invalid argument name: ${name}`);
3879
+ }
3880
+ return new ExpressionRef({
3881
+ kind: "namedArg",
3882
+ name,
3883
+ value: toExpressionIntent(value)
3884
+ });
3885
+ }
3886
+ function star() {
3887
+ return new ExpressionRef({ kind: "star" });
3888
+ }
3889
+ function array(...items) {
3890
+ return new ExpressionRef({
3891
+ kind: "array",
3892
+ elements: items.map(toExpressionIntent)
3893
+ });
3894
+ }
3895
+ function isDistinctField2(input) {
3896
+ return !Array.isArray(input) && !(input instanceof ExpressionRef) && isDistinctField(input);
3897
+ }
3898
+ function isAggOrderByArg(input) {
3899
+ return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof ExpressionRef) && "__aggOrderBy" in input && input.__aggOrderBy === true;
3900
+ }
3901
+ function aggOrderBy(field, direction = "asc") {
3902
+ return { __aggOrderBy: true, field, direction };
3903
+ }
3904
+ function arrayAgg(col2, ...rest) {
3905
+ const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3906
+ return fn("array_agg", colExpr, ...rest);
3907
+ }
3908
+ function stringAgg(col2, separator, ...rest) {
3909
+ const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3910
+ return fn("string_agg", colExpr, separator, ...rest);
3911
+ }
3912
+
3913
+ // src/dx/case-when-builder.ts
3914
+ function toResultIntent(value) {
3915
+ if (value instanceof ExpressionRef) {
3916
+ return value.intent;
3917
+ }
3918
+ if (value === null || value === void 0) {
3919
+ return { kind: "literal", value: null };
3920
+ }
3921
+ if (typeof value === "string") {
3922
+ return { kind: "ref", column: value };
3923
+ }
3924
+ return { kind: "literal", value };
3925
+ }
3926
+ var CaseBuilder = class _CaseBuilder {
3927
+ branches;
3928
+ constructor(branches) {
3929
+ this.branches = branches;
3930
+ }
3931
+ when(condition, thenValue) {
3932
+ return new _CaseBuilder([
3933
+ ...this.branches,
3934
+ { condition, result: toResultIntent(thenValue) }
3935
+ ]);
3936
+ }
3937
+ else(elseValue) {
3938
+ const intent = {
3939
+ kind: "case",
3940
+ when: this.branches,
3941
+ else: toResultIntent(elseValue)
3942
+ };
3943
+ return new ExpressionRef(intent);
3944
+ }
3945
+ as(alias) {
3946
+ return this.toExpr().as(alias);
3947
+ }
3948
+ toExpr() {
3949
+ const intent = {
3950
+ kind: "case",
3951
+ when: this.branches
3952
+ };
3953
+ return new ExpressionRef(intent);
3954
+ }
3955
+ };
3956
+ function caseWhen(condition, thenValue) {
3957
+ return new CaseBuilder([{ condition, result: toResultIntent(thenValue) }]);
3958
+ }
3959
+
3960
+ // src/dx/builder-utils.ts
3961
+ function requireAdapter(adapter, operationName) {
3962
+ if (!adapter) {
3963
+ throw new InvalidOperationError(
3964
+ operationName,
3965
+ "This operation requires an adapter \u2014 pass an adapter when creating the ORM"
3966
+ );
3967
+ }
3968
+ return adapter;
3969
+ }
3970
+
3971
+ // src/dx/cte-builder.ts
3972
+ var CteBuilder = class {
3973
+ cteName;
3974
+ adapter;
3975
+ schemaName;
3976
+ unnestColumns;
3977
+ indexColumnName;
3978
+ constructor(name, adapter, schemaName) {
3979
+ this.cteName = name;
3980
+ this.adapter = adapter;
3981
+ this.schemaName = schemaName;
3982
+ }
3983
+ /**
3984
+ * Provide the column arrays for the unnest() CTE.
3985
+ * All arrays must have the same length.
3986
+ *
3987
+ * @param columns - A record of column name → array of values.
3988
+ */
3989
+ fromUnnest(columns) {
3990
+ const lengths = Object.values(columns).map((arr) => arr.length);
3991
+ if (lengths.length > 1 && !lengths.every((l) => l === lengths[0])) {
3992
+ throw new InvalidOperationError(
3993
+ "withCte",
3994
+ `Array length mismatch in CTE unnest columns: lengths are [${lengths.join(", ")}]`
3995
+ );
3996
+ }
3997
+ this.unnestColumns = columns;
3998
+ return this;
3999
+ }
4000
+ /**
4001
+ * Add a 0-based ordinality index column (uses WITH ORDINALITY).
4002
+ *
4003
+ * @param columnName - The name of the index column in the CTE.
4004
+ */
4005
+ withIndex(columnName) {
4006
+ this.indexColumnName = columnName;
4007
+ return this;
4008
+ }
4009
+ /**
4010
+ * Attach an outer query and produce a CteQueryBuilder for execution.
4011
+ *
4012
+ * @param selectBuilder - A QueryBuilder (from orm.select(...))
4013
+ */
4014
+ query(selectBuilder) {
4015
+ if (!this.unnestColumns) {
4016
+ throw new InvalidOperationError(
4017
+ "withCte",
4018
+ "CTE requires a data source \u2014 call .fromUnnest() first"
4019
+ );
4020
+ }
4021
+ const cteIntent = {
4022
+ kind: "unnestCte",
4023
+ name: this.cteName,
4024
+ columns: this.unnestColumns,
4025
+ ...this.indexColumnName !== void 0 && {
4026
+ indexColumn: this.indexColumnName
4027
+ }
4028
+ };
4029
+ return new CteQueryBuilder(
4030
+ cteIntent,
4031
+ selectBuilder,
4032
+ this.adapter,
4033
+ this.schemaName
4034
+ );
4035
+ }
4036
+ };
4037
+ var CteQueryBuilder = class {
4038
+ cteIntent;
4039
+ outerBuilder;
4040
+ adapter;
4041
+ schemaName;
4042
+ constructor(cteIntent, outerBuilder, adapter, schemaName) {
4043
+ this.cteIntent = cteIntent;
4044
+ this.outerBuilder = outerBuilder;
4045
+ this.adapter = adapter;
4046
+ this.schemaName = schemaName;
4047
+ }
4048
+ /**
4049
+ * Build the CteQueryIntent AST.
4050
+ */
4051
+ buildIntent() {
4052
+ const queryIntent = this.outerBuilder.buildIntent();
4053
+ return {
4054
+ kind: "cteQuery",
4055
+ ctes: [this.cteIntent],
4056
+ query: queryIntent
4057
+ };
4058
+ }
4059
+ requireAdapter() {
4060
+ return requireAdapter(this.adapter, "withCte");
4061
+ }
4062
+ /**
4063
+ * Compile to SQL and return an observability dump.
4064
+ */
4065
+ dump() {
4066
+ const adapter = this.requireAdapter();
4067
+ const intent = this.buildIntent();
4068
+ const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
4069
+ const compiled = adapter.compileCteQuery(
4070
+ intent,
4071
+ compileOptions
4072
+ );
4073
+ return {
4074
+ sql: compiled.sql,
4075
+ params: compiled.parameters,
4076
+ intent
4077
+ };
4078
+ }
4079
+ /**
4080
+ * Execute the CTE query and return all results.
4081
+ */
4082
+ async all() {
4083
+ const adapter = this.requireAdapter();
4084
+ const intent = this.buildIntent();
4085
+ const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
4086
+ const compiled = adapter.compileCteQuery(
4087
+ intent,
4088
+ compileOptions
4089
+ );
4090
+ return adapter.execute(compiled);
4091
+ }
4092
+ /**
4093
+ * Alias for all().
4094
+ */
3583
4095
  execute() {
3584
4096
  return this.all();
3585
4097
  }
@@ -3621,628 +4133,245 @@ var DEFAULT_FEATURE_CHECKERS = Object.freeze([
3621
4133
  capability: "supportsDDLPartitioning",
3622
4134
  feature: "partition",
3623
4135
  detectUsage(model) {
3624
- if (!model.tables) return [];
3625
- const usages = [];
3626
- for (const [tableName, table] of model.tables) {
3627
- if (table.partition) {
3628
- usages.push({ table: tableName, detail: tableName });
3629
- }
3630
- }
3631
- return usages;
3632
- }
3633
- },
3634
- {
3635
- capability: "supportsDDLComments",
3636
- feature: "comment",
3637
- detectUsage(model) {
3638
- if (!model.tables) return [];
3639
- const usages = [];
3640
- for (const [tableName, table] of model.tables) {
3641
- if (table.comment) {
3642
- usages.push({ table: tableName, detail: `${tableName} (table)` });
3643
- }
3644
- for (const col2 of table.columns) {
3645
- if (col2.comment) {
3646
- usages.push({
3647
- table: tableName,
3648
- column: col2.name,
3649
- detail: `${tableName}.${col2.name} (column)`
3650
- });
3651
- }
3652
- }
3653
- }
3654
- return usages;
3655
- }
3656
- },
3657
- {
3658
- capability: "supportsDDLCheckConstraints",
3659
- feature: "checkConstraint",
3660
- detectUsage(model) {
3661
- if (!model.tables) return [];
3662
- const usages = [];
3663
- for (const [tableName, table] of model.tables) {
3664
- if (table.checkConstraints?.length) {
3665
- for (const chk of table.checkConstraints) {
3666
- usages.push({
3667
- table: tableName,
3668
- detail: `${tableName}.${chk.name}`
3669
- });
3670
- }
3671
- }
3672
- }
3673
- return usages;
3674
- }
3675
- },
3676
- {
3677
- capability: "supportsDDLOnUpdateFK",
3678
- feature: "onUpdateFK",
3679
- detectUsage(model) {
3680
- if (!model.tables) return [];
3681
- const usages = [];
3682
- for (const [tableName, table] of model.tables) {
3683
- for (const fk of table.foreignKeys) {
3684
- if (fk.onUpdate && fk.onUpdate !== "NO ACTION") {
3685
- usages.push({
3686
- table: tableName,
3687
- detail: `${tableName} FK \u2192 ${fk.references.table}`
3688
- });
3689
- }
3690
- }
3691
- }
3692
- return usages;
3693
- }
3694
- },
3695
- {
3696
- capability: "supportsDDLDeferredFK",
3697
- feature: "deferredFK",
3698
- detectUsage(model) {
3699
- if (!model.tables) return [];
3700
- const usages = [];
3701
- for (const [tableName, table] of model.tables) {
3702
- for (const fk of table.foreignKeys) {
3703
- if (fk.deferred) {
3704
- usages.push({
3705
- table: tableName,
3706
- detail: `${tableName} FK \u2192 ${fk.references.table}`
3707
- });
3708
- }
3709
- }
3710
- }
3711
- return usages;
3712
- }
3713
- },
3714
- // -----------------------------------------------------------------------
3715
- // Column-level features
3716
- // -----------------------------------------------------------------------
3717
- {
3718
- capability: "supportsDDLIdentityColumns",
3719
- feature: "identity",
3720
- detectUsage(model) {
3721
- if (!model.tables) return [];
3722
- const usages = [];
3723
- for (const [tableName, table] of model.tables) {
3724
- for (const col2 of table.columns) {
3725
- if (col2.identity) {
3726
- usages.push({
3727
- table: tableName,
3728
- column: col2.name,
3729
- detail: `${tableName}.${col2.name}`
3730
- });
3731
- }
3732
- }
3733
- }
3734
- return usages;
3735
- }
3736
- },
3737
- {
3738
- capability: "supportsDDLCollation",
3739
- feature: "collation",
3740
- detectUsage(model) {
3741
- if (!model.tables) return [];
3742
- const usages = [];
3743
- for (const [tableName, table] of model.tables) {
3744
- for (const col2 of table.columns) {
3745
- if (col2.collation) {
3746
- usages.push({
3747
- table: tableName,
3748
- column: col2.name,
3749
- detail: `${tableName}.${col2.name}`
3750
- });
3751
- }
3752
- }
3753
- }
3754
- return usages;
3755
- }
3756
- },
3757
- // -----------------------------------------------------------------------
3758
- // Index-level features
3759
- // -----------------------------------------------------------------------
3760
- {
3761
- capability: "supportsDDLIndexMethods",
3762
- feature: "indexMethod",
3763
- detectUsage(model) {
3764
- if (!model.tables) return [];
3765
- const usages = [];
3766
- for (const [tableName, table] of model.tables) {
3767
- for (const idx of table.indexes) {
3768
- if (idx.method && idx.method !== "btree") {
3769
- const idxName = idx.name ?? `idx on ${tableName}`;
3770
- usages.push({ table: tableName, detail: idxName });
3771
- }
3772
- }
3773
- }
3774
- return usages;
3775
- }
3776
- },
3777
- {
3778
- capability: "supportsDDLIndexOpclass",
3779
- feature: "indexOpclass",
3780
- detectUsage(model) {
3781
- if (!model.tables) return [];
3782
- const usages = [];
3783
- for (const [tableName, table] of model.tables) {
3784
- for (const idx of table.indexes) {
3785
- if (idx.opclass && Object.keys(idx.opclass).length > 0) {
3786
- const idxName = idx.name ?? `idx on ${tableName}`;
3787
- usages.push({ table: tableName, detail: idxName });
3788
- }
3789
- }
3790
- }
3791
- return usages;
3792
- }
3793
- },
3794
- {
3795
- capability: "supportsDDLIndexInclude",
3796
- feature: "indexInclude",
3797
- detectUsage(model) {
3798
- if (!model.tables) return [];
3799
- const usages = [];
3800
- for (const [tableName, table] of model.tables) {
3801
- for (const idx of table.indexes) {
3802
- if (idx.include?.length) {
3803
- const idxName = idx.name ?? `idx on ${tableName}`;
3804
- usages.push({ table: tableName, detail: idxName });
3805
- }
3806
- }
3807
- }
3808
- return usages;
3809
- }
3810
- },
3811
- {
3812
- capability: "supportsDDLPartialIndexes",
3813
- feature: "partialIndex",
3814
- detectUsage(model) {
3815
- if (!model.tables) return [];
3816
- const usages = [];
3817
- for (const [tableName, table] of model.tables) {
3818
- for (const idx of table.indexes) {
3819
- if (idx.where) {
3820
- const idxName = idx.name ?? `idx on ${tableName}`;
3821
- usages.push({ table: tableName, detail: idxName });
3822
- }
3823
- }
3824
- }
3825
- return usages;
3826
- }
3827
- },
3828
- {
3829
- capability: "supportsDDLExpressionIndexes",
3830
- feature: "expressionIndex",
3831
- detectUsage(model) {
3832
- if (!model.tables) return [];
3833
- const usages = [];
3834
- for (const [tableName, table] of model.tables) {
3835
- for (const idx of table.indexes) {
3836
- if (idx.expressions?.length) {
3837
- const idxName = idx.name ?? `idx on ${tableName}`;
3838
- usages.push({ table: tableName, detail: idxName });
3839
- }
3840
- }
3841
- }
3842
- return usages;
3843
- }
3844
- },
3845
- // -----------------------------------------------------------------------
3846
- // Row-Level Security
3847
- // -----------------------------------------------------------------------
3848
- {
3849
- capability: "supportsDDLRowLevelSecurity",
3850
- feature: "rowLevelSecurity",
3851
- detectUsage(model) {
3852
- if (!model.tables) return [];
3853
- const usages = [];
3854
- for (const [tableName, table] of model.tables) {
3855
- if (table.rlsEnabled || table.policies?.length) {
3856
- usages.push({ table: tableName, detail: tableName });
3857
- }
3858
- }
3859
- return usages;
3860
- }
3861
- }
3862
- ]);
3863
-
3864
- // src/dx/column-utils.ts
3865
- function getColumnName(field) {
3866
- if (typeof field === "string") {
3867
- return field;
3868
- }
3869
- const colName = field[COLUMN_META];
3870
- if (colName === void 0) {
3871
- throw new Error("Invalid ColumnRef: missing COLUMN_META");
3872
- }
3873
- return colName;
3874
- }
3875
-
3876
- // src/dx/window-functions.ts
3877
- var WindowBuilder = class _WindowBuilder {
3878
- constructor(fnKind, partitions = [], orders = []) {
3879
- this.fnKind = fnKind;
3880
- this.partitions = partitions;
3881
- this.orders = orders;
3882
- }
3883
- fnKind;
3884
- partitions;
3885
- orders;
3886
- /**
3887
- * Create a ranking window builder (row_number, rank, dense_rank)
3888
- * @internal Use factory functions instead: rowNumber(), rank(), denseRank()
3889
- */
3890
- static ranking(fn2) {
3891
- return new _WindowBuilder({ type: "ranking", fn: fn2 });
3892
- }
3893
- /**
3894
- * Create an aggregate window builder (sum, avg, count, min, max)
3895
- * @internal Use factory functions instead: sum(field), avg(field), etc.
3896
- */
3897
- static aggregate(fn2, field) {
3898
- return new _WindowBuilder({
3899
- type: "aggregate",
3900
- fn: fn2,
3901
- ...field !== void 0 ? { field } : {}
3902
- });
3903
- }
3904
- /**
3905
- * Create an offset window builder (lag, lead)
3906
- * @internal Use factory functions instead: lag(field), lead(field)
3907
- */
3908
- static offset(fn2, field) {
3909
- return new _WindowBuilder({ type: "offset", fn: fn2, field });
3910
- }
3911
- /**
3912
- * Add partition field(s) to the OVER clause.
3913
- * Multiple calls APPEND fields (not replace).
3914
- * Supports both string field names and ColumnRef (DX-040).
3915
- *
3916
- * @example
3917
- * sum('amount').partitionBy('user_id').partitionBy('category')
3918
- * // → PARTITION BY "user_id", "category"
3919
- *
3920
- * @example DX-040 with ColumnRef
3921
- * rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
3922
- */
3923
- partitionBy(...fields) {
3924
- const fieldNames = fields.map(
3925
- (f) => typeof f === "string" ? f : getColumnName(f)
3926
- );
3927
- return new _WindowBuilder(
3928
- this.fnKind,
3929
- [...this.partitions, ...fieldNames],
3930
- this.orders
3931
- );
3932
- }
3933
- /**
3934
- * Add order field to the OVER clause.
3935
- * Multiple calls APPEND fields (not replace).
3936
- * Supports both string field names and ColumnRef (DX-040).
3937
- *
3938
- * @param field - Column name or ColumnRef to order by
3939
- * @param direction - Sort direction: 'asc' (default) or 'desc'
3940
- *
3941
- * @example
3942
- * rowNumber().orderBy('created_at').orderBy('id', 'desc')
3943
- * // → ORDER BY "created_at" ASC, "id" DESC
3944
- *
3945
- * @example DX-040 with ColumnRef
3946
- * rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
3947
- */
3948
- orderBy(field, direction = "asc") {
3949
- const fieldName = typeof field === "string" ? field : getColumnName(field);
3950
- return new _WindowBuilder(this.fnKind, this.partitions, [
3951
- ...this.orders,
3952
- { field: fieldName, direction }
3953
- ]);
3954
- }
3955
- /**
3956
- * Finalize the window expression with an alias.
3957
- * Returns ExpressionSpec for use in columns().
3958
- *
3959
- * @param alias - Required alias for the result column
3960
- *
3961
- * @example
3962
- * columns(['id', rowNumber().orderBy('date').as('rn')])
3963
- */
3964
- as(alias) {
3965
- return {
3966
- __expr: true,
3967
- intent: this.toWindowIntent(alias)
3968
- };
3969
- }
3970
- /**
3971
- * Convert builder state to WindowIntent — produces the correct discriminated branch.
3972
- * - ranking → RankingWindowIntent (no field)
3973
- * - aggregate → AggregateWindowIntent (field required; COUNT uses '*' when omitted)
3974
- * - offset → OffsetWindowIntent (field required)
3975
- */
3976
- toWindowIntent(alias) {
3977
- const over = {};
3978
- if (this.partitions.length > 0) {
3979
- over.partitionBy = this.partitions;
3980
- }
3981
- if (this.orders.length > 0) {
3982
- over.orderBy = this.orders;
3983
- }
3984
- if (this.fnKind.type === "ranking") {
3985
- return {
3986
- kind: "window",
3987
- function: this.fnKind.fn,
3988
- alias,
3989
- over
3990
- };
3991
- }
3992
- if (this.fnKind.type === "offset") {
3993
- return {
3994
- kind: "window",
3995
- function: this.fnKind.fn,
3996
- field: this.fnKind.field,
3997
- alias,
3998
- over
3999
- };
4000
- }
4001
- const { field } = this.fnKind;
4002
- return {
4003
- kind: "window",
4004
- function: this.fnKind.fn,
4005
- ...field !== void 0 && { field },
4006
- alias,
4007
- over
4008
- };
4009
- }
4010
- };
4011
- function rowNumber() {
4012
- return WindowBuilder.ranking("row_number");
4013
- }
4014
- function rank() {
4015
- return WindowBuilder.ranking("rank");
4016
- }
4017
- function denseRank() {
4018
- return WindowBuilder.ranking("dense_rank");
4019
- }
4020
- function wSum(field) {
4021
- return WindowBuilder.aggregate("sum", field);
4022
- }
4023
- function wAvg(field) {
4024
- return WindowBuilder.aggregate("avg", field);
4025
- }
4026
- function wCount(field) {
4027
- return WindowBuilder.aggregate("count", field);
4028
- }
4029
- function wMin(field) {
4030
- return WindowBuilder.aggregate("min", field);
4031
- }
4032
- function wMax(field) {
4033
- return WindowBuilder.aggregate("max", field);
4034
- }
4035
- function lag(field) {
4036
- return WindowBuilder.offset("lag", field);
4037
- }
4038
- function lead(field) {
4039
- return WindowBuilder.offset("lead", field);
4040
- }
4041
-
4042
- // src/dx/filters.ts
4043
- function distinct(field) {
4044
- return { field, distinct: true };
4045
- }
4046
- function isDistinctField(value) {
4047
- return typeof value === "object" && value !== null && "field" in value && "distinct" in value && value.distinct === true;
4048
- }
4049
- function createComparisonFilter(operator) {
4050
- return (field, value) => ({
4051
- kind: "comparison",
4052
- field: getColumnName(field),
4053
- operator,
4054
- value
4055
- });
4056
- }
4057
- var eq = createComparisonFilter("eq");
4058
- var neq = createComparisonFilter("neq");
4059
- var gt = createComparisonFilter("gt");
4060
- var gte = createComparisonFilter("gte");
4061
- var lt = createComparisonFilter("lt");
4062
- var lte = createComparisonFilter("lte");
4063
- var isDistinctFrom = createComparisonFilter("isDistinctFrom");
4064
- function like(field, pattern, options) {
4065
- const caseInsensitive = typeof options === "boolean" ? options : options?.caseInsensitive;
4066
- const escapeChar = typeof options === "object" ? options.escape : void 0;
4067
- const intent = {
4068
- kind: "like",
4069
- field: getColumnName(field),
4070
- pattern
4071
- };
4072
- const withCi = caseInsensitive !== void 0 ? { ...intent, caseInsensitive } : intent;
4073
- if (escapeChar !== void 0) {
4074
- return { ...withCi, escape: escapeChar };
4075
- }
4076
- return withCi;
4077
- }
4078
- function inArray(field, values) {
4079
- return { kind: "in", field: getColumnName(field), values };
4080
- }
4081
- function inSubquery(field, query) {
4082
- const expr = "build" in query ? query.build() : query;
4083
- return {
4084
- kind: "in",
4085
- field: getColumnName(field),
4086
- subquery: expr.toIntent()
4087
- };
4088
- }
4089
- function any(field, values) {
4090
- return { kind: "any", field: getColumnName(field), values };
4091
- }
4092
- function isNull(field) {
4093
- return { kind: "null", field: getColumnName(field), operator: "isNull" };
4094
- }
4095
- function isNotNull(field) {
4096
- return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
4097
- }
4098
- function and(...conditions) {
4099
- const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
4100
- return { kind: "and", conditions: flatConditions };
4101
- }
4102
- function or(...conditions) {
4103
- const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
4104
- return { kind: "or", conditions: flatConditions };
4105
- }
4106
- function not(condition) {
4107
- return { kind: "not", condition };
4108
- }
4109
- function exists(relation, options) {
4110
- const result = { kind: "exists", relation };
4111
- if (options?.where !== void 0) {
4112
- result.where = options.where;
4113
- }
4114
- if (options?.recursive !== void 0) {
4115
- result.recursive = options.recursive;
4116
- }
4117
- if (options?.include !== void 0) {
4118
- result.include = options.include;
4119
- }
4120
- return result;
4121
- }
4122
- function notExists(relation, options) {
4123
- const result = {
4124
- kind: "notExists",
4125
- relation
4126
- };
4127
- if (options?.where !== void 0) {
4128
- result.where = options.where;
4129
- }
4130
- if (options?.recursive !== void 0) {
4131
- result.recursive = options.recursive;
4132
- }
4133
- if (options?.include !== void 0) {
4134
- result.include = options.include;
4135
- }
4136
- return result;
4137
- }
4138
- function rawExists(sq) {
4139
- const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
4140
- return { kind: "rawExists", subquery: intent };
4141
- }
4142
- function rawNotExists(sq) {
4143
- const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
4144
- return { kind: "rawNotExists", subquery: intent };
4145
- }
4146
- function getRelationName(rel) {
4147
- const meta = rel[RELATION_META];
4148
- if (!meta) {
4149
- throw new Error("Invalid RelationRef: missing RELATION_META");
4150
- }
4151
- return meta.target;
4152
- }
4153
- function every(relation, filter) {
4154
- const relationName = getRelationName(relation);
4155
- const where = filter(relation);
4156
- return {
4157
- kind: "relationFilter",
4158
- relation: relationName,
4159
- where,
4160
- mode: "every"
4161
- };
4162
- }
4163
- function none(relation, filter) {
4164
- const relationName = getRelationName(relation);
4165
- const where = filter(relation);
4166
- return {
4167
- kind: "relationFilter",
4168
- relation: relationName,
4169
- where,
4170
- mode: "none"
4171
- };
4172
- }
4173
- function some(relation, filter) {
4174
- const relationName = getRelationName(relation);
4175
- const where = filter(relation);
4176
- return {
4177
- kind: "relationFilter",
4178
- relation: relationName,
4179
- where,
4180
- mode: "some"
4181
- };
4182
- }
4183
- function coalesce(fields, as) {
4184
- if (fields.length === 0) {
4185
- throw new Error("coalesce() requires at least one field");
4186
- }
4187
- if (!as || as.trim() === "") {
4188
- throw new Error("coalesce() requires a non-empty alias");
4189
- }
4190
- for (const f of fields) {
4191
- validateIdentifier(f, "column");
4192
- }
4193
- validateIdentifier(as, "column");
4194
- return {
4195
- __expr: true,
4196
- intent: { kind: "coalesce", fields, as }
4197
- };
4198
- }
4199
- function raw(sqlFragment, as) {
4200
- if (typeof as !== "string" || as.trim().length === 0) {
4201
- throw new InvalidOperationError(
4202
- "raw",
4203
- "raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
4204
- );
4205
- }
4206
- validateIdentifier(as, "column");
4207
- return {
4208
- __expr: true,
4209
- intent: { kind: "raw", sql: sqlFragment, as }
4210
- };
4211
- }
4212
- function col(column, alias) {
4213
- validateIdentifier(column, "column");
4214
- validateIdentifier(alias, "column");
4215
- return {
4216
- __expr: true,
4217
- intent: { kind: "columnAlias", column, alias }
4218
- };
4219
- }
4220
- function relationColumn(relation, column, as) {
4221
- if (!relation) {
4222
- throw new Error("relationColumn() requires a non-empty relation path");
4223
- }
4224
- for (const segment of relation.split(".")) {
4225
- validateIdentifier(segment, "relation");
4226
- }
4227
- if (column !== "*") {
4228
- validateIdentifier(column, "column");
4229
- }
4230
- validateIdentifier(as, "column");
4231
- return {
4232
- __expr: true,
4233
- intent: { kind: "relationColumn", relation, column, as }
4234
- };
4235
- }
4236
- var SQL_RAW_MARKER = /* @__PURE__ */ Symbol.for("dbsp:raw-sql");
4237
- function sql(sqlFragment) {
4238
- if (!sqlFragment || sqlFragment.trim() === "") {
4239
- throw new Error("sql() requires a non-empty SQL fragment");
4136
+ if (!model.tables) return [];
4137
+ const usages = [];
4138
+ for (const [tableName, table] of model.tables) {
4139
+ if (table.partition) {
4140
+ usages.push({ table: tableName, detail: tableName });
4141
+ }
4142
+ }
4143
+ return usages;
4144
+ }
4145
+ },
4146
+ {
4147
+ capability: "supportsDDLComments",
4148
+ feature: "comment",
4149
+ detectUsage(model) {
4150
+ if (!model.tables) return [];
4151
+ const usages = [];
4152
+ for (const [tableName, table] of model.tables) {
4153
+ if (table.comment) {
4154
+ usages.push({ table: tableName, detail: `${tableName} (table)` });
4155
+ }
4156
+ for (const col2 of table.columns) {
4157
+ if (col2.comment) {
4158
+ usages.push({
4159
+ table: tableName,
4160
+ column: col2.name,
4161
+ detail: `${tableName}.${col2.name} (column)`
4162
+ });
4163
+ }
4164
+ }
4165
+ }
4166
+ return usages;
4167
+ }
4168
+ },
4169
+ {
4170
+ capability: "supportsDDLCheckConstraints",
4171
+ feature: "checkConstraint",
4172
+ detectUsage(model) {
4173
+ if (!model.tables) return [];
4174
+ const usages = [];
4175
+ for (const [tableName, table] of model.tables) {
4176
+ if (table.checkConstraints?.length) {
4177
+ for (const chk of table.checkConstraints) {
4178
+ usages.push({
4179
+ table: tableName,
4180
+ detail: `${tableName}.${chk.name}`
4181
+ });
4182
+ }
4183
+ }
4184
+ }
4185
+ return usages;
4186
+ }
4187
+ },
4188
+ {
4189
+ capability: "supportsDDLOnUpdateFK",
4190
+ feature: "onUpdateFK",
4191
+ detectUsage(model) {
4192
+ if (!model.tables) return [];
4193
+ const usages = [];
4194
+ for (const [tableName, table] of model.tables) {
4195
+ for (const fk of table.foreignKeys) {
4196
+ if (fk.onUpdate && fk.onUpdate !== "NO ACTION") {
4197
+ usages.push({
4198
+ table: tableName,
4199
+ detail: `${tableName} FK \u2192 ${fk.references.table}`
4200
+ });
4201
+ }
4202
+ }
4203
+ }
4204
+ return usages;
4205
+ }
4206
+ },
4207
+ {
4208
+ capability: "supportsDDLDeferredFK",
4209
+ feature: "deferredFK",
4210
+ detectUsage(model) {
4211
+ if (!model.tables) return [];
4212
+ const usages = [];
4213
+ for (const [tableName, table] of model.tables) {
4214
+ for (const fk of table.foreignKeys) {
4215
+ if (fk.deferred) {
4216
+ usages.push({
4217
+ table: tableName,
4218
+ detail: `${tableName} FK \u2192 ${fk.references.table}`
4219
+ });
4220
+ }
4221
+ }
4222
+ }
4223
+ return usages;
4224
+ }
4225
+ },
4226
+ // -----------------------------------------------------------------------
4227
+ // Column-level features
4228
+ // -----------------------------------------------------------------------
4229
+ {
4230
+ capability: "supportsDDLIdentityColumns",
4231
+ feature: "identity",
4232
+ detectUsage(model) {
4233
+ if (!model.tables) return [];
4234
+ const usages = [];
4235
+ for (const [tableName, table] of model.tables) {
4236
+ for (const col2 of table.columns) {
4237
+ if (col2.identity) {
4238
+ usages.push({
4239
+ table: tableName,
4240
+ column: col2.name,
4241
+ detail: `${tableName}.${col2.name}`
4242
+ });
4243
+ }
4244
+ }
4245
+ }
4246
+ return usages;
4247
+ }
4248
+ },
4249
+ {
4250
+ capability: "supportsDDLCollation",
4251
+ feature: "collation",
4252
+ detectUsage(model) {
4253
+ if (!model.tables) return [];
4254
+ const usages = [];
4255
+ for (const [tableName, table] of model.tables) {
4256
+ for (const col2 of table.columns) {
4257
+ if (col2.collation) {
4258
+ usages.push({
4259
+ table: tableName,
4260
+ column: col2.name,
4261
+ detail: `${tableName}.${col2.name}`
4262
+ });
4263
+ }
4264
+ }
4265
+ }
4266
+ return usages;
4267
+ }
4268
+ },
4269
+ // -----------------------------------------------------------------------
4270
+ // Index-level features
4271
+ // -----------------------------------------------------------------------
4272
+ {
4273
+ capability: "supportsDDLIndexMethods",
4274
+ feature: "indexMethod",
4275
+ detectUsage(model) {
4276
+ if (!model.tables) return [];
4277
+ const usages = [];
4278
+ for (const [tableName, table] of model.tables) {
4279
+ for (const idx of table.indexes) {
4280
+ if (idx.method && idx.method !== "btree") {
4281
+ const idxName = idx.name ?? `idx on ${tableName}`;
4282
+ usages.push({ table: tableName, detail: idxName });
4283
+ }
4284
+ }
4285
+ }
4286
+ return usages;
4287
+ }
4288
+ },
4289
+ {
4290
+ capability: "supportsDDLIndexOpclass",
4291
+ feature: "indexOpclass",
4292
+ detectUsage(model) {
4293
+ if (!model.tables) return [];
4294
+ const usages = [];
4295
+ for (const [tableName, table] of model.tables) {
4296
+ for (const idx of table.indexes) {
4297
+ if (idx.opclass && Object.keys(idx.opclass).length > 0) {
4298
+ const idxName = idx.name ?? `idx on ${tableName}`;
4299
+ usages.push({ table: tableName, detail: idxName });
4300
+ }
4301
+ }
4302
+ }
4303
+ return usages;
4304
+ }
4305
+ },
4306
+ {
4307
+ capability: "supportsDDLIndexInclude",
4308
+ feature: "indexInclude",
4309
+ detectUsage(model) {
4310
+ if (!model.tables) return [];
4311
+ const usages = [];
4312
+ for (const [tableName, table] of model.tables) {
4313
+ for (const idx of table.indexes) {
4314
+ if (idx.include?.length) {
4315
+ const idxName = idx.name ?? `idx on ${tableName}`;
4316
+ usages.push({ table: tableName, detail: idxName });
4317
+ }
4318
+ }
4319
+ }
4320
+ return usages;
4321
+ }
4322
+ },
4323
+ {
4324
+ capability: "supportsDDLPartialIndexes",
4325
+ feature: "partialIndex",
4326
+ detectUsage(model) {
4327
+ if (!model.tables) return [];
4328
+ const usages = [];
4329
+ for (const [tableName, table] of model.tables) {
4330
+ for (const idx of table.indexes) {
4331
+ if (idx.where) {
4332
+ const idxName = idx.name ?? `idx on ${tableName}`;
4333
+ usages.push({ table: tableName, detail: idxName });
4334
+ }
4335
+ }
4336
+ }
4337
+ return usages;
4338
+ }
4339
+ },
4340
+ {
4341
+ capability: "supportsDDLExpressionIndexes",
4342
+ feature: "expressionIndex",
4343
+ detectUsage(model) {
4344
+ if (!model.tables) return [];
4345
+ const usages = [];
4346
+ for (const [tableName, table] of model.tables) {
4347
+ for (const idx of table.indexes) {
4348
+ if (idx.expressions?.length) {
4349
+ const idxName = idx.name ?? `idx on ${tableName}`;
4350
+ usages.push({ table: tableName, detail: idxName });
4351
+ }
4352
+ }
4353
+ }
4354
+ return usages;
4355
+ }
4356
+ },
4357
+ // -----------------------------------------------------------------------
4358
+ // Row-Level Security
4359
+ // -----------------------------------------------------------------------
4360
+ {
4361
+ capability: "supportsDDLRowLevelSecurity",
4362
+ feature: "rowLevelSecurity",
4363
+ detectUsage(model) {
4364
+ if (!model.tables) return [];
4365
+ const usages = [];
4366
+ for (const [tableName, table] of model.tables) {
4367
+ if (table.rlsEnabled || table.policies?.length) {
4368
+ usages.push({ table: tableName, detail: tableName });
4369
+ }
4370
+ }
4371
+ return usages;
4372
+ }
4240
4373
  }
4241
- return { [SQL_RAW_MARKER]: true, sql: sqlFragment };
4242
- }
4243
- function isSqlRaw(value) {
4244
- return typeof value === "object" && value !== null && SQL_RAW_MARKER in value && value[SQL_RAW_MARKER] === true;
4245
- }
4374
+ ]);
4246
4375
 
4247
4376
  // src/dx/full-text-search.ts
4248
4377
  function fullTextSearch({
@@ -10299,16 +10428,13 @@ function buildIndexAPI(tableName, schemaName, adapter, requireAdapter2) {
10299
10428
  "indexes.list() requires an adapter."
10300
10429
  );
10301
10430
  }
10302
- if (adapter.listIndexes) {
10303
- return adapter.listIndexes(tableName, schemaName, options);
10304
- }
10305
- const sql2 = `SELECT indexname AS name, indexdef AS definition, (SELECT indisunique FROM pg_index WHERE indexrelid = (SELECT oid FROM pg_class WHERE relname = indexname)) AS unique, CASE WHEN indexdef LIKE '%USING %' THEN split_part(indexdef, 'USING ', 2) ELSE 'btree' END AS method FROM pg_indexes WHERE tablename = '${tableName.replace(/'/g, "''")}' ` + (schemaName ? `AND schemaname = '${schemaName.replace(/'/g, "''")}' ` : "") + `ORDER BY indexname`;
10306
- if ("executeRaw" in adapter && typeof adapter.executeRaw === "function") {
10307
- const rows = await adapter.executeRaw(sql2, []);
10308
- return rows;
10431
+ if (!adapter.listIndexes) {
10432
+ throw new InvalidOperationError(
10433
+ "indexes.list",
10434
+ "indexes.list() requires an adapter that implements listIndexes()."
10435
+ );
10309
10436
  }
10310
- if (adapter.executeDDL) await adapter.executeDDL(sql2);
10311
- return [];
10437
+ return adapter.listIndexes(tableName, schemaName, options);
10312
10438
  },
10313
10439
  async exists(name) {
10314
10440
  if (!adapter) {
@@ -10933,6 +11059,14 @@ function rangeContainedBy(fieldOrColumn, valueOrRange, rangeType = "daterange")
10933
11059
  // src/dx/schema-bridge.ts
10934
11060
  import { buildRelationKeyFields, toColumnList as toColumnList3 } from "@dbsp/types";
10935
11061
  import * as v from "valibot";
11062
+ function normalizeReferenceSchema2(schemaName) {
11063
+ if (schemaName == null) return void 0;
11064
+ const t = schemaName.trim();
11065
+ return t.length > 0 ? t : void 0;
11066
+ }
11067
+ function hasExternalSchema3(schemaName) {
11068
+ return normalizeReferenceSchema2(schemaName) !== void 0;
11069
+ }
10936
11070
  function generatedTypeToColumnType(genType) {
10937
11071
  switch (genType) {
10938
11072
  case "string":
@@ -11005,11 +11139,11 @@ function findRelationForeignKey(sourceTable, genRelation, tables) {
11005
11139
  switch (genRelation.kind) {
11006
11140
  case "belongsTo":
11007
11141
  return tables.get(sourceTable)?.foreignKeys.find(
11008
- (fk) => fk.references.table === genRelation.target && columnListsEqual(fk.columns, genRelation.foreignKey)
11142
+ (fk) => !hasExternalSchema3(fk.references.schema) && fk.references.table === genRelation.target && columnListsEqual(fk.columns, genRelation.foreignKey)
11009
11143
  );
11010
11144
  case "hasMany":
11011
11145
  return tables.get(genRelation.target)?.foreignKeys.find(
11012
- (fk) => fk.references.table === sourceTable && columnListsEqual(fk.columns, genRelation.foreignKey)
11146
+ (fk) => !hasExternalSchema3(fk.references.schema) && fk.references.table === sourceTable && columnListsEqual(fk.columns, genRelation.foreignKey)
11013
11147
  );
11014
11148
  case "manyToMany":
11015
11149
  return void 0;
@@ -11044,11 +11178,13 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
11044
11178
  primaryKeys.push(colName);
11045
11179
  }
11046
11180
  if (colDef.references) {
11181
+ const externalSchema = normalizeReferenceSchema2(colDef.references.schema);
11047
11182
  const fk = {
11048
11183
  columns: [colName],
11049
11184
  references: {
11050
11185
  table: colDef.references.table,
11051
- columns: [colDef.references.column ?? "id"]
11186
+ columns: [colDef.references.column ?? "id"],
11187
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {}
11052
11188
  }
11053
11189
  };
11054
11190
  if (colDef.references.onDelete) {
@@ -11078,7 +11214,7 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
11078
11214
  for (const fk of foreignKeys) {
11079
11215
  const fkColumn = fk.columns[0];
11080
11216
  const targetColumn = fk.references.columns[0];
11081
- if (fk.references.table === tableName && fkColumn !== void 0 && targetColumn !== void 0) {
11217
+ if (fk.references.table === tableName && !hasExternalSchema3(fk.references.schema) && fkColumn !== void 0 && targetColumn !== void 0) {
11082
11218
  const colDef = tableColumns[fkColumn];
11083
11219
  const inferredName = fkColumn.endsWith("Id") ? fkColumn.slice(0, -2) : "parent";
11084
11220
  const parentRole = colDef?.references?.parentRole ?? inferredName;
@@ -11111,11 +11247,13 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
11111
11247
  }
11112
11248
  if (isGeneratedTableWithConfig(genTable)) {
11113
11249
  for (const fk of genTable.foreignKeys ?? []) {
11250
+ const externalSchema = normalizeReferenceSchema2(fk.references.schema);
11114
11251
  const foreignKey = {
11115
11252
  columns: [...fk.columns],
11116
11253
  references: {
11117
11254
  table: fk.references.table,
11118
- columns: [...fk.references.columns]
11255
+ columns: [...fk.references.columns],
11256
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {}
11119
11257
  }
11120
11258
  };
11121
11259
  if (fk.onDelete) foreignKey.onDelete = fk.onDelete;
@@ -11332,12 +11470,25 @@ function safeRecord(valueSchema) {
11332
11470
  v.record(v.string(), valueSchema)
11333
11471
  );
11334
11472
  }
11473
+ var OptionalReferenceSchemaNameSchema = v.optional(
11474
+ v.pipe(
11475
+ v.string(),
11476
+ v.transform((schemaName) => normalizeReferenceSchema2(schemaName))
11477
+ )
11478
+ );
11479
+ var ForeignKeyActionValues = [
11480
+ "CASCADE",
11481
+ "SET NULL",
11482
+ "SET DEFAULT",
11483
+ "RESTRICT",
11484
+ "NO ACTION"
11485
+ ];
11486
+ var ForeignKeyActionSchema = v.picklist(ForeignKeyActionValues);
11335
11487
  var ForeignKeyReferenceSchema = v.object({
11488
+ schema: OptionalReferenceSchemaNameSchema,
11336
11489
  table: v.string(),
11337
11490
  column: v.optional(v.string()),
11338
- onDelete: v.optional(
11339
- v.picklist(["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"])
11340
- ),
11491
+ onDelete: v.optional(ForeignKeyActionSchema),
11341
11492
  index: v.optional(v.boolean()),
11342
11493
  parentRole: v.optional(v.string()),
11343
11494
  childRole: v.optional(v.string())
@@ -11362,15 +11513,12 @@ var TableDefWithConfigSchema = v.object({
11362
11513
  v.object({
11363
11514
  columns: v.array(v.string()),
11364
11515
  references: v.object({
11516
+ schema: OptionalReferenceSchemaNameSchema,
11365
11517
  table: v.string(),
11366
11518
  columns: v.array(v.string())
11367
11519
  }),
11368
- onDelete: v.optional(
11369
- v.picklist(["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"])
11370
- ),
11371
- onUpdate: v.optional(
11372
- v.picklist(["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"])
11373
- )
11520
+ onDelete: v.optional(ForeignKeyActionSchema),
11521
+ onUpdate: v.optional(ForeignKeyActionSchema)
11374
11522
  })
11375
11523
  )
11376
11524
  ),
@@ -11504,8 +11652,10 @@ function convertColumn(col2) {
11504
11652
  result.default = col2.default;
11505
11653
  }
11506
11654
  if (col2.references) {
11655
+ const externalSchema = normalizeReferenceSchema2(col2.references.schema);
11507
11656
  result.references = {
11508
11657
  table: col2.references.table,
11658
+ ...externalSchema !== void 0 ? { schema: externalSchema } : {},
11509
11659
  ...col2.references.column !== void 0 ? { column: col2.references.column } : {},
11510
11660
  ...col2.references.onDelete ? { onDelete: col2.references.onDelete } : {},
11511
11661
  ...col2.references.index !== void 0 ? { index: col2.references.index } : {},