@dbsp/core 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -645,7 +645,12 @@ function schemaToModelIR(definition, constraints, extras, options) {
645
645
  options
646
646
  );
647
647
  validateFkTargets(tables);
648
- const relations = buildRelations(definition, refsByTable, tableNames);
648
+ const relations = buildRelations(
649
+ definition,
650
+ refsByTable,
651
+ tableNames,
652
+ constraints
653
+ );
649
654
  const tableMap = /* @__PURE__ */ new Map();
650
655
  for (const table of tables) {
651
656
  tableMap.set(table.name, table);
@@ -1057,6 +1062,7 @@ function buildTableConstraints(tableName, constraints) {
1057
1062
  }
1058
1063
  };
1059
1064
  if (fkRef.options.onDelete) fk.onDelete = fkRef.options.onDelete;
1065
+ if (fkRef.options.onUpdate) fk.onUpdate = fkRef.options.onUpdate;
1060
1066
  extraForeignKeys.push(fk);
1061
1067
  }
1062
1068
  }
@@ -1095,7 +1101,7 @@ function normalizeColumnDef(def) {
1095
1101
  }
1096
1102
  return def;
1097
1103
  }
1098
- function buildRelations(_definition, refsByTable, tableNames) {
1104
+ function buildRelations(_definition, refsByTable, tableNames, constraints) {
1099
1105
  const relations = [];
1100
1106
  for (const tableName of tableNames) {
1101
1107
  const refs = refsByTable.get(tableName) || [];
@@ -1195,8 +1201,75 @@ function buildRelations(_definition, refsByTable, tableNames) {
1195
1201
  }
1196
1202
  }
1197
1203
  }
1204
+ addCompositeConstraintRelations(relations, tableNames, constraints);
1198
1205
  return relations;
1199
1206
  }
1207
+ function relationKey(source, name) {
1208
+ return `${source}.${name}`;
1209
+ }
1210
+ function pushRelationIfAbsent(relations, relationKeys, relation) {
1211
+ const key = relationKey(relation.source, relation.name);
1212
+ if (relationKeys.has(key)) return;
1213
+ relationKeys.add(key);
1214
+ relations.push(relation);
1215
+ }
1216
+ function deriveCompositeConstraintRelationName(fkColumn, options) {
1217
+ return deriveLocalRelation(fkColumn, options);
1218
+ }
1219
+ function addCompositeConstraintRelations(relations, tableNames, constraints) {
1220
+ if (!constraints) return;
1221
+ const tableSet = new Set(tableNames);
1222
+ const relationKeys = new Set(
1223
+ relations.map((relation) => relationKey(relation.source, relation.name))
1224
+ );
1225
+ for (const tableName of tableNames) {
1226
+ const foreignKeys = constraints[tableName]?.foreignKeys;
1227
+ if (!foreignKeys) continue;
1228
+ for (const fkRef of foreignKeys) {
1229
+ if (!tableSet.has(fkRef.target)) continue;
1230
+ const columns = fkRef.options.columns;
1231
+ if (!columns?.length) continue;
1232
+ const references = fkRef.options.references ?? ["id"];
1233
+ if (columns.length === 1 && references.length === 1) continue;
1234
+ const foreignKey = [...columns];
1235
+ const referencedKey = [...references];
1236
+ const belongsToName = deriveCompositeConstraintRelationName(
1237
+ foreignKey[0],
1238
+ fkRef.options
1239
+ );
1240
+ const inverseName = fkRef.options.inverse ?? tableName;
1241
+ const isUnique = fkRef.options.unique ?? false;
1242
+ const inverseType = isUnique ? "hasOne" : "hasMany";
1243
+ const inverseCardinality = isUnique ? "one" : "many";
1244
+ pushRelationIfAbsent(relations, relationKeys, {
1245
+ name: belongsToName,
1246
+ type: "belongsTo",
1247
+ source: tableName,
1248
+ target: fkRef.target,
1249
+ foreignKey,
1250
+ targetKey: referencedKey,
1251
+ cardinality: "one",
1252
+ optionality: fkRef.options.nullable ? "optional" : "required",
1253
+ includeStrategy: "auto",
1254
+ filterStrategy: "auto",
1255
+ joinDefault: "auto"
1256
+ });
1257
+ pushRelationIfAbsent(relations, relationKeys, {
1258
+ name: inverseName,
1259
+ type: inverseType,
1260
+ source: fkRef.target,
1261
+ target: tableName,
1262
+ foreignKey,
1263
+ sourceKey: referencedKey,
1264
+ cardinality: inverseCardinality,
1265
+ optionality: "optional",
1266
+ includeStrategy: "auto",
1267
+ filterStrategy: "auto",
1268
+ joinDefault: "auto"
1269
+ });
1270
+ }
1271
+ }
1272
+ }
1200
1273
  function columnTypeToJsType(type) {
1201
1274
  switch (type) {
1202
1275
  // Numeric types → 'number' (includes integer, bigint, decimal)
@@ -1591,6 +1664,9 @@ function isManyToMany(rel) {
1591
1664
  return rel.kind === "manyToMany";
1592
1665
  }
1593
1666
 
1667
+ // src/planner.ts
1668
+ import { toColumnList } from "@dbsp/types";
1669
+
1594
1670
  // src/dx/errors.ts
1595
1671
  var ExecutionError = class _ExecutionError extends Error {
1596
1672
  name = "ExecutionError";
@@ -2325,14 +2401,21 @@ function generateBidirectionalReasoning(storageHint, strategy) {
2325
2401
  function isSubquerySelectedColumnNonNullable(existsIntent, sourceTable, model) {
2326
2402
  const rel = model.getRelation(`${sourceTable}.${existsIntent.relation}`);
2327
2403
  if (!rel) return false;
2328
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
2329
- if (!fk) return false;
2404
+ const fkColumns = toColumnList(rel.foreignKey);
2405
+ if (fkColumns.length !== 1) return false;
2406
+ const fk = fkColumns[0];
2330
2407
  const targetTableIR = model.getTable(rel.target);
2331
2408
  if (!targetTableIR) return false;
2332
2409
  const column = targetTableIR.columns.find((c) => c.name === fk);
2333
2410
  if (!column) return false;
2334
2411
  return !column.nullable;
2335
2412
  }
2413
+ function resolveTargetJsonAggOrderKey(model, targetTable) {
2414
+ const table = model.getTable(targetTable);
2415
+ if (!table) return { columns: [], fallback: false };
2416
+ const pkColumns = toColumnList(table.primaryKey);
2417
+ return pkColumns.length > 0 ? { columns: pkColumns, fallback: false } : { columns: table.columns.map((col2) => col2.name), fallback: true };
2418
+ }
2336
2419
  function optimizeInToExists(where, sourceTable, model, negated = false) {
2337
2420
  switch (where.kind) {
2338
2421
  case "in": {
@@ -2343,18 +2426,22 @@ function optimizeInToExists(where, sourceTable, model, negated = false) {
2343
2426
  return where;
2344
2427
  }
2345
2428
  const subSelect = inWhere.subquery.select;
2346
- if (!subSelect || subSelect.type !== "fields") return where;
2429
+ if (subSelect?.type !== "fields") return where;
2347
2430
  const fields = "fields" in subSelect ? subSelect.fields : void 0;
2348
- if (!fields || fields.length !== 1) return where;
2431
+ if (fields?.length !== 1) return where;
2349
2432
  const subColumn = fields[0];
2350
2433
  if (!subColumn) return where;
2351
2434
  const relationsFrom = model.getRelationsFrom(sourceTable);
2352
2435
  const sourceTableIR = model.getTable(sourceTable);
2353
- const sourcePk = typeof sourceTableIR?.primaryKey === "string" ? sourceTableIR.primaryKey : sourceTableIR?.primaryKey?.[0] ?? "id";
2436
+ const sourcePkColumns = toColumnList(sourceTableIR?.primaryKey);
2437
+ if (sourcePkColumns.length > 1) return where;
2438
+ const sourcePk = sourcePkColumns[0] ?? "id";
2354
2439
  let matchedRelation;
2355
2440
  for (const rel of relationsFrom) {
2356
2441
  if (rel.target !== inWhere.subquery.from) continue;
2357
- const fk = typeof rel.foreignKey === "string" ? rel.foreignKey : rel.foreignKey?.[0];
2442
+ const fkColumns = toColumnList(rel.foreignKey);
2443
+ if (fkColumns.length !== 1) continue;
2444
+ const fk = fkColumns[0];
2358
2445
  if (rel.type === "hasMany" && fk === subColumn && inWhere.field === sourcePk) {
2359
2446
  matchedRelation = rel.name;
2360
2447
  break;
@@ -2656,6 +2743,8 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2656
2743
  const cascadedJoinType = ancestorIsLeftJoin && include.join === void 0 ? "left" : autoJoinType;
2657
2744
  const explicitJoinType = includeStrategy === "join" ? include.join ?? cascadedJoinType : void 0;
2658
2745
  const includeDecisionId = generateDecisionId(state, "include-strategy");
2746
+ const parentKey = relation.type === "belongsTo" ? relation.targetKey : relation.sourceKey;
2747
+ const targetOrder = resolveTargetJsonAggOrderKey(model, relation.target);
2659
2748
  state.decisions.push({
2660
2749
  id: includeDecisionId,
2661
2750
  type: "include-strategy",
@@ -2669,7 +2758,12 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2669
2758
  // Foreign key info for json_agg compilation (Phase 3)
2670
2759
  ...relation.foreignKey !== void 0 && {
2671
2760
  foreignKey: relation.foreignKey
2672
- }
2761
+ },
2762
+ ...parentKey !== void 0 && { parentKey },
2763
+ ...targetOrder.columns.length > 0 && {
2764
+ targetPrimaryKey: targetOrder.columns
2765
+ },
2766
+ ...targetOrder.fallback && { orderByFallback: true }
2673
2767
  },
2674
2768
  choice: includeStrategy,
2675
2769
  // Embed joinType so the adapter's join handler can use it directly
@@ -6163,10 +6257,98 @@ import {
6163
6257
  NqlLexer,
6164
6258
  compile as nqlCompile
6165
6259
  } from "@dbsp/nql";
6260
+ import { toColumnList as toColumnList2 } from "@dbsp/types";
6166
6261
  import {
6262
+ explainUnsupportedNqlBindingIncludeHop,
6167
6263
  getTrustedNqlRelationFilterFields,
6168
6264
  NQL_INTERNAL_COMPILER_OPTIONS
6169
6265
  } from "@dbsp/types/internal";
6266
+
6267
+ // src/dx/hydration-utils.ts
6268
+ function isRootIncludeDecision(decision) {
6269
+ const intentPath = decision.context.intentPath;
6270
+ return typeof intentPath !== "string" || !intentPath.includes(".include[");
6271
+ }
6272
+ function hydrateJsonAggIncludes(results, planReport) {
6273
+ const jsonAggDecisions = planReport.decisions.filter(
6274
+ (d) => d.type === "include-strategy" && d.choice === "json_agg" && isRootIncludeDecision(d)
6275
+ );
6276
+ if (jsonAggDecisions.length === 0) {
6277
+ return;
6278
+ }
6279
+ const relationInfo = /* @__PURE__ */ new Map();
6280
+ for (const decision of jsonAggDecisions) {
6281
+ const canonicalName = decision.context?.relation;
6282
+ const includeAlias = decision.context?.includeAlias;
6283
+ const relationType = decision.context?.relationType;
6284
+ const isToOne = relationType === "belongsTo" || relationType === "hasOne";
6285
+ if (typeof canonicalName === "string") {
6286
+ relationInfo.set(canonicalName, {
6287
+ isToOne,
6288
+ includeAlias: typeof includeAlias === "string" ? includeAlias : canonicalName,
6289
+ canonicalName
6290
+ });
6291
+ } else if (typeof includeAlias === "string") {
6292
+ relationInfo.set(includeAlias, { isToOne, includeAlias });
6293
+ }
6294
+ }
6295
+ if (relationInfo.size === 0) {
6296
+ return;
6297
+ }
6298
+ for (const row of results) {
6299
+ if (typeof row !== "object" || row === null) {
6300
+ continue;
6301
+ }
6302
+ const record2 = row;
6303
+ for (const [relationName, info] of relationInfo) {
6304
+ const candidates = [relationName];
6305
+ if (info.includeAlias && info.includeAlias !== relationName) {
6306
+ candidates.push(info.includeAlias);
6307
+ }
6308
+ let actualColumnName = null;
6309
+ for (const baseName of candidates) {
6310
+ const snakeJson = `${baseName}_json`;
6311
+ const camelJson = snakeJson.replace(
6312
+ /_([a-z])/g,
6313
+ (_, c) => c.toUpperCase()
6314
+ );
6315
+ if (snakeJson in record2) {
6316
+ actualColumnName = snakeJson;
6317
+ break;
6318
+ }
6319
+ if (camelJson in record2) {
6320
+ actualColumnName = camelJson;
6321
+ break;
6322
+ }
6323
+ }
6324
+ if (actualColumnName) {
6325
+ const jsonValue = record2[actualColumnName];
6326
+ let parsed;
6327
+ if (typeof jsonValue === "string") {
6328
+ try {
6329
+ parsed = JSON.parse(jsonValue);
6330
+ } catch {
6331
+ parsed = info.isToOne ? null : [];
6332
+ }
6333
+ } else if (Array.isArray(jsonValue)) {
6334
+ parsed = jsonValue;
6335
+ } else if (jsonValue === null || jsonValue === void 0) {
6336
+ parsed = info.isToOne ? null : [];
6337
+ } else {
6338
+ parsed = jsonValue;
6339
+ }
6340
+ if (info.isToOne && Array.isArray(parsed)) {
6341
+ parsed = parsed.length > 0 ? parsed[0] : null;
6342
+ }
6343
+ const outputKey = info.includeAlias ?? relationName;
6344
+ record2[outputKey] = parsed;
6345
+ delete record2[actualColumnName];
6346
+ }
6347
+ }
6348
+ }
6349
+ }
6350
+
6351
+ // src/dx/nql.ts
6170
6352
  var NQL_RAW_FRAGMENT = /* @__PURE__ */ Symbol("NqlRawFragment");
6171
6353
  function hasNqlBindings(bundle) {
6172
6354
  return (bundle.bindings?.size ?? 0) > 0;
@@ -6203,28 +6385,340 @@ function findBindingFinalRelationFilter(where) {
6203
6385
  return void 0;
6204
6386
  }
6205
6387
  }
6206
- function assertBindingFinalQueryCanUseSyntheticPlan(intent) {
6388
+ function bindingFinalIncludeError(bindingName, relation, reason) {
6389
+ return new Error(
6390
+ `NQL binding-final query '${bindingName}' cannot use relation include '${relation}' (ref-#192): ${reason}.`
6391
+ );
6392
+ }
6393
+ function getBindingRelationMetadata(bundle, bindingName) {
6394
+ return bundle.bindingOutputSchemas?.get(bindingName)?.relationFilters;
6395
+ }
6396
+ function getScalarBindingRelationsByName(bundle, bindingName) {
6397
+ const metadata = getBindingRelationMetadata(bundle, bindingName);
6398
+ const relations = /* @__PURE__ */ new Map();
6399
+ for (const relation of metadata?.scalarRelations ?? []) {
6400
+ relations.set(relation.relation, relation);
6401
+ }
6402
+ return relations;
6403
+ }
6404
+ function bindingIncludeNodeShape(include) {
6405
+ return include;
6406
+ }
6407
+ function virtualRelationIncludeShape(relation) {
6408
+ const relationType = relation.relationType;
6409
+ const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relationType === "hasOne" || relationType === "hasMany" ? relation.targetColumn : void 0;
6410
+ return {
6411
+ type: relationType,
6412
+ foreignKey,
6413
+ source: relation.sourceTable,
6414
+ target: relation.targetTable
6415
+ };
6416
+ }
6417
+ var BINDING_INCLUDE_NODE_ONLY_RELATION = {
6418
+ type: "hasMany",
6419
+ foreignKey: "__dbsp_binding_include_node__",
6420
+ source: "__dbsp_binding_include_source__",
6421
+ target: "__dbsp_binding_include_target__"
6422
+ };
6423
+ function assertSupportedBindingIncludeNodeShape(bindingName, rootIncludeRelation, include, reasonPrefix = "") {
6424
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
6425
+ include.relation,
6426
+ BINDING_INCLUDE_NODE_ONLY_RELATION,
6427
+ bindingIncludeNodeShape(include)
6428
+ );
6429
+ if (unsupportedReason) {
6430
+ throw bindingFinalIncludeError(
6431
+ bindingName,
6432
+ rootIncludeRelation,
6433
+ `${reasonPrefix}${unsupportedReason}`
6434
+ );
6435
+ }
6436
+ }
6437
+ function bindingIncludeCoversRelationPath(include, segments) {
6438
+ const [head, ...tail] = segments;
6439
+ if (!head || include.relation !== head) return false;
6440
+ if (tail.length === 0) return true;
6441
+ return (include.include ?? []).some(
6442
+ (child) => bindingIncludeCoversRelationPath(child, tail)
6443
+ );
6444
+ }
6445
+ function bindingIncludesCoverRelationPath(includes, relationPath) {
6446
+ const segments = relationPath.split(".");
6447
+ if (segments.some((segment) => segment.length === 0)) return false;
6448
+ return (includes ?? []).some(
6449
+ (include) => bindingIncludeCoversRelationPath(include, segments)
6450
+ );
6451
+ }
6452
+ function trustedBindingRelationColumnIsAdmitted(column) {
6453
+ if (column.kind !== "relationColumn") return false;
6454
+ const trusted = getTrustedNqlRelationFilterFields(column);
6455
+ if (trusted?.selectedColumn === void 0) return false;
6456
+ const isDotted = column.relation.split(".").length > 1;
6457
+ if (trusted.cardinality === "one") {
6458
+ return !isDotted || trusted.hops.length > 0;
6459
+ }
6460
+ return trusted.cardinality === "many" && trusted.relationType === "hasMany" && !isDotted && trusted.hops.length === 0;
6461
+ }
6462
+ function assertProvenBindingInclude(bindingName, include, provenRelations) {
6463
+ const proven = provenRelations.get(include.relation);
6464
+ if (!proven) {
6465
+ throw bindingFinalIncludeError(
6466
+ bindingName,
6467
+ include.relation,
6468
+ "the relation is not in the binding proven virtual-relation set"
6469
+ );
6470
+ }
6471
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
6472
+ include.relation,
6473
+ virtualRelationIncludeShape(proven),
6474
+ bindingIncludeNodeShape(include)
6475
+ );
6476
+ if (unsupportedReason) {
6477
+ throw bindingFinalIncludeError(
6478
+ bindingName,
6479
+ include.relation,
6480
+ unsupportedReason
6481
+ );
6482
+ }
6483
+ return proven;
6484
+ }
6485
+ function getProvenBindingIncludes(intent, bundle) {
6486
+ const provenRelations = getScalarBindingRelationsByName(bundle, intent.from);
6487
+ const provenIncludes = /* @__PURE__ */ new Map();
6488
+ for (const include of intent.include ?? []) {
6489
+ const proven = assertProvenBindingInclude(
6490
+ intent.from,
6491
+ include,
6492
+ provenRelations
6493
+ );
6494
+ provenIncludes.set(include.relation, proven);
6495
+ }
6496
+ return provenIncludes;
6497
+ }
6498
+ function assertBindingFinalQueryCanUseSyntheticPlan(intent, bundle) {
6499
+ getProvenBindingIncludes(intent, bundle);
6207
6500
  const relationColumns = intent.select?.type === "expressions" ? intent.select.columns.flatMap((column) => {
6208
6501
  if (column.kind !== "relationColumn") return [];
6209
- const trusted = getTrustedNqlRelationFilterFields(column);
6210
- if (trusted?.selectedColumn !== void 0 && trusted.cardinality === "one") {
6502
+ if (column.column === "*" && bindingIncludesCoverRelationPath(intent.include, column.relation)) {
6503
+ return [];
6504
+ }
6505
+ if (trustedBindingRelationColumnIsAdmitted(column)) {
6211
6506
  return [];
6212
6507
  }
6213
6508
  return [column.relation];
6214
6509
  }) : [];
6215
6510
  const relationFilter = intent.where ? findBindingFinalRelationFilter(intent.where) : void 0;
6216
6511
  const havingRelationFilter = intent.having ? findBindingFinalRelationFilter(intent.having) : void 0;
6217
- if ((intent.include?.length ?? 0) > 0 || relationColumns.length > 0 || (intent.joins?.length ?? 0) > 0 || relationFilter !== void 0 || havingRelationFilter !== void 0) {
6512
+ if (relationColumns.length > 0 || (intent.joins?.length ?? 0) > 0 || relationFilter !== void 0 || havingRelationFilter !== void 0) {
6218
6513
  throw new Error(
6219
6514
  `NQL binding-final query '${intent.from}' cannot select relation columns, use includes, joins, or relation filters. Relation planning requires a physical model table, not a CTE binding.`
6220
6515
  );
6221
6516
  }
6222
6517
  }
6223
- function createBindingFinalPlan(intent) {
6224
- assertBindingFinalQueryCanUseSyntheticPlan(intent);
6518
+ function createBindingIncludeDecision(intent, include, relation, index, model) {
6519
+ const relationType = relation.relationType;
6520
+ const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relation.targetColumn;
6521
+ const parentKey = relationType === "belongsTo" ? relation.targetColumn : relation.sourceColumn;
6522
+ const targetOrder = resolveTargetJsonAggOrderKey2(model, relation.targetTable);
6523
+ return {
6524
+ id: `binding-include-${index}`,
6525
+ type: "include-strategy",
6526
+ context: {
6527
+ sourceTable: intent.from,
6528
+ target: relation.targetTable,
6529
+ relation: relation.relation,
6530
+ relationType,
6531
+ foreignKey,
6532
+ parentKey,
6533
+ ...targetOrder.columns.length > 0 && {
6534
+ targetPrimaryKey: targetOrder.columns
6535
+ },
6536
+ ...targetOrder.fallback && { orderByFallback: true },
6537
+ includeAlias: include.relation,
6538
+ intentPath: `include[${index}]`
6539
+ },
6540
+ choice: "json_agg",
6541
+ reasoning: "NQL binding-final include uses proven binding virtual relation metadata and the json_agg include pipeline.",
6542
+ alternatives: []
6543
+ };
6544
+ }
6545
+ function findModelRelation(model, sourceTable, relationName) {
6546
+ return model.getRelation(`${sourceTable}.${relationName}`) ?? model.getRelationsFrom(sourceTable).find((relation) => relation.name === relationName);
6547
+ }
6548
+ function parentKeyForRelation(relation) {
6549
+ if (relation.type === "belongsTo") return relation.targetKey;
6550
+ return relation.sourceKey;
6551
+ }
6552
+ function resolveTargetJsonAggOrderKey2(model, targetTable) {
6553
+ const table = model.getTable(targetTable);
6554
+ if (!table) return { columns: [], fallback: false };
6555
+ const pkColumns = toColumnList2(table.primaryKey);
6556
+ return pkColumns.length > 0 ? { columns: pkColumns, fallback: false } : { columns: table.columns.map((col2) => col2.name), fallback: true };
6557
+ }
6558
+ function assertSupportedBindingIncludeHop(bindingName, rootIncludeRelation, relationName, relation, include, reasonPrefix = "") {
6559
+ const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
6560
+ relationName,
6561
+ relation,
6562
+ bindingIncludeNodeShape(include)
6563
+ );
6564
+ if (unsupportedReason) {
6565
+ throw bindingFinalIncludeError(
6566
+ bindingName,
6567
+ rootIncludeRelation,
6568
+ `${reasonPrefix}${unsupportedReason}`
6569
+ );
6570
+ }
6571
+ }
6572
+ function assertSupportedBindingTailIncludeTree(bindingName, rootInclude, sourceTable, includes, model) {
6573
+ for (const include of includes) {
6574
+ assertSupportedBindingIncludeNodeShape(
6575
+ bindingName,
6576
+ rootInclude.relation,
6577
+ include,
6578
+ "tail "
6579
+ );
6580
+ const relation = findModelRelation(model, sourceTable, include.relation);
6581
+ if (!relation) {
6582
+ throw bindingFinalIncludeError(
6583
+ bindingName,
6584
+ rootInclude.relation,
6585
+ `tail relation '${include.relation}' is not declared on table '${sourceTable}'`
6586
+ );
6587
+ }
6588
+ assertSupportedBindingIncludeHop(
6589
+ bindingName,
6590
+ rootInclude.relation,
6591
+ include.relation,
6592
+ relation,
6593
+ include,
6594
+ "tail "
6595
+ );
6596
+ assertSupportedBindingTailIncludeTree(
6597
+ bindingName,
6598
+ rootInclude,
6599
+ relation.target,
6600
+ include.include ?? [],
6601
+ model
6602
+ );
6603
+ }
6604
+ }
6605
+ function rebaseBindingTailIntentPath(intentPath, includeIndex) {
6606
+ return `include[${includeIndex}]${intentPath ? `.${intentPath}` : ""}`;
6607
+ }
6608
+ function createBindingTailIncludeDecisions(bindingName, include, firstHopRelation, includeIndex, model) {
6609
+ const nestedIncludes = include.include ?? [];
6610
+ if (nestedIncludes.length === 0) return [];
6611
+ assertSupportedBindingTailIncludeTree(
6612
+ bindingName,
6613
+ include,
6614
+ firstHopRelation.targetTable,
6615
+ nestedIncludes,
6616
+ model
6617
+ );
6618
+ const tailPlan = plan(
6619
+ {
6620
+ type: "select",
6621
+ from: firstHopRelation.targetTable,
6622
+ include: nestedIncludes
6623
+ },
6624
+ model,
6625
+ { defaultIncludeStrategy: "json_agg" }
6626
+ );
6627
+ return tailPlan.decisions.filter((decision) => decision.type === "include-strategy").map((decision, tailIndex) => {
6628
+ const sourceTable = decision.context.sourceTable;
6629
+ const relationName = decision.context.relation;
6630
+ if (!relationName) {
6631
+ throw bindingFinalIncludeError(
6632
+ bindingName,
6633
+ include.relation,
6634
+ "tail include planning produced a decision without a relation name"
6635
+ );
6636
+ }
6637
+ const relation = findModelRelation(model, sourceTable, relationName);
6638
+ if (!relation) {
6639
+ throw bindingFinalIncludeError(
6640
+ bindingName,
6641
+ include.relation,
6642
+ `tail relation '${relationName}' is not declared on table '${sourceTable}'`
6643
+ );
6644
+ }
6645
+ assertSupportedBindingIncludeHop(
6646
+ bindingName,
6647
+ include.relation,
6648
+ relationName,
6649
+ relation,
6650
+ { relation: relationName },
6651
+ "tail "
6652
+ );
6653
+ const baseContext = { ...decision.context };
6654
+ delete baseContext.foreignKey;
6655
+ delete baseContext.parentKey;
6656
+ delete baseContext.targetPrimaryKey;
6657
+ delete baseContext.orderByFallback;
6658
+ const parentKey = parentKeyForRelation(relation);
6659
+ const targetOrder = resolveTargetJsonAggOrderKey2(model, relation.target);
6660
+ return {
6661
+ ...decision,
6662
+ id: `binding-include-${includeIndex}-tail-${tailIndex}`,
6663
+ choice: "json_agg",
6664
+ reasoning: "NQL binding-final tail include uses real-table relation metadata and is forced through the json_agg include pipeline.",
6665
+ context: {
6666
+ ...baseContext,
6667
+ sourceTable: relation.source,
6668
+ target: relation.target,
6669
+ relation: relation.name,
6670
+ relationType: relation.type,
6671
+ ...relation.foreignKey !== void 0 && {
6672
+ foreignKey: relation.foreignKey
6673
+ },
6674
+ // Leave parentKey absent when RelationIR does not specify it so the
6675
+ // adapter applies the same defaultPkColumnName fallback as real-table includes.
6676
+ ...parentKey !== void 0 && { parentKey },
6677
+ ...targetOrder.columns.length > 0 && {
6678
+ targetPrimaryKey: targetOrder.columns
6679
+ },
6680
+ ...targetOrder.fallback && { orderByFallback: true },
6681
+ intentPath: rebaseBindingTailIntentPath(
6682
+ decision.context.intentPath,
6683
+ includeIndex
6684
+ )
6685
+ }
6686
+ };
6687
+ });
6688
+ }
6689
+ function createBindingFinalPlan(intent, bundle, model, dialectCapabilities) {
6690
+ assertBindingFinalQueryCanUseSyntheticPlan(intent, bundle);
6691
+ const provenIncludes = getProvenBindingIncludes(intent, bundle);
6692
+ const decisions = (intent.include ?? []).flatMap((include, index) => {
6693
+ if (dialectCapabilities?.supportsJsonAgg === false) {
6694
+ throw bindingFinalIncludeError(
6695
+ intent.from,
6696
+ include.relation,
6697
+ "JSON aggregation for binding includes is not supported by this adapter"
6698
+ );
6699
+ }
6700
+ const proven = provenIncludes.get(include.relation);
6701
+ const firstHopDecision = createBindingIncludeDecision(
6702
+ intent,
6703
+ include,
6704
+ proven,
6705
+ index,
6706
+ model
6707
+ );
6708
+ return [
6709
+ firstHopDecision,
6710
+ ...createBindingTailIncludeDecisions(
6711
+ intent.from,
6712
+ include,
6713
+ proven,
6714
+ index,
6715
+ model
6716
+ )
6717
+ ];
6718
+ });
6225
6719
  return {
6226
6720
  rootTable: intent.from,
6227
- decisions: [],
6721
+ decisions,
6228
6722
  warnings: [],
6229
6723
  ctes: [],
6230
6724
  intent,
@@ -6235,6 +6729,11 @@ function createBindingFinalPlan(intent) {
6235
6729
  }
6236
6730
  };
6237
6731
  }
6732
+ function bindingFinalPlanHasJsonAggIncludes(planReport) {
6733
+ return planReport.decisions.some(
6734
+ (decision) => decision.type === "include-strategy" && decision.choice === "json_agg"
6735
+ );
6736
+ }
6238
6737
  function nqlRaw(fragment) {
6239
6738
  if (typeof fragment !== "string") {
6240
6739
  throw new TypeError("nqlRaw() expects a string fragment");
@@ -6694,7 +7193,12 @@ var NqlBuilderImpl = class {
6694
7193
  "NQL mutations do not have execution plans; use dump() for SQL and parameters."
6695
7194
  );
6696
7195
  }
6697
- return isBindingFinalQuery(compiled.bundle) ? createBindingFinalPlan(compiled.intent) : this.planInternal();
7196
+ return isBindingFinalQuery(compiled.bundle) ? createBindingFinalPlan(
7197
+ compiled.intent,
7198
+ compiled.bundle,
7199
+ this.model,
7200
+ this.adapter?.dialectCapabilities
7201
+ ) : this.planInternal();
6698
7202
  }
6699
7203
  dump(meta) {
6700
7204
  const compiledIntent = this.compile();
@@ -6709,7 +7213,12 @@ var NqlBuilderImpl = class {
6709
7213
  );
6710
7214
  }
6711
7215
  const bindingFinalQuery = isBindingFinalQuery(compiledIntent.bundle);
6712
- const planReport = bindingFinalQuery ? createBindingFinalPlan(compiledIntent.intent) : this.planInternal();
7216
+ const planReport = bindingFinalQuery ? createBindingFinalPlan(
7217
+ compiledIntent.intent,
7218
+ compiledIntent.bundle,
7219
+ this.model,
7220
+ this.adapter?.dialectCapabilities
7221
+ ) : this.planInternal();
6713
7222
  if (!this.adapter) {
6714
7223
  return {
6715
7224
  plan: planReport,
@@ -6718,7 +7227,10 @@ var NqlBuilderImpl = class {
6718
7227
  ...meta !== void 0 && { meta }
6719
7228
  };
6720
7229
  }
6721
- const finalBundle = this.createFinalNqlStatementBundle(compiledIntent);
7230
+ const finalBundle = this.createFinalNqlStatementBundle(
7231
+ compiledIntent,
7232
+ bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
7233
+ );
6722
7234
  const compiled = bindingFinalQuery || hasNqlBindings(finalBundle) ? this.adapter.compile(finalBundle, this.nqlBundleCompileOptions()) : this.adapter.compile(planReport);
6723
7235
  try {
6724
7236
  return this.adapter.createDump(planReport, compiled, meta);
@@ -6770,7 +7282,7 @@ var NqlBuilderImpl = class {
6770
7282
  ...this._schemaName !== void 0 && { schemaName: this._schemaName }
6771
7283
  };
6772
7284
  }
6773
- createNqlStatementBundle(statement, bindings, runtimeBindings, sourceBundle, bindingDependencies) {
7285
+ createNqlStatementBundle(statement, bindings, runtimeBindings, sourceBundle, bindingDependencies, planReport) {
6774
7286
  const statementBindings = filterMapByRuntimeBindingDependencies(
6775
7287
  bindings,
6776
7288
  sourceBundle.mutationBindings,
@@ -6794,6 +7306,7 @@ var NqlBuilderImpl = class {
6794
7306
  );
6795
7307
  return {
6796
7308
  ...statement,
7309
+ ...planReport !== void 0 && { plan: planReport },
6797
7310
  ...statementBindings.size > 0 && { bindings: statementBindings },
6798
7311
  ...statementBindingOutputSchemas !== void 0 && {
6799
7312
  bindingOutputSchemas: statementBindingOutputSchemas
@@ -6806,7 +7319,7 @@ var NqlBuilderImpl = class {
6806
7319
  }
6807
7320
  };
6808
7321
  }
6809
- createFinalNqlStatementBundle(compiledIntent) {
7322
+ createFinalNqlStatementBundle(compiledIntent, planReport) {
6810
7323
  const finalStep = createNqlProgramSteps(compiledIntent).at(-1);
6811
7324
  if (compiledIntent.kind === "query") {
6812
7325
  return this.createNqlStatementBundle(
@@ -6814,7 +7327,8 @@ var NqlBuilderImpl = class {
6814
7327
  compiledIntent.bundle.bindings ?? /* @__PURE__ */ new Map(),
6815
7328
  compiledIntent.bundle.runtimeBindings ?? /* @__PURE__ */ new Map(),
6816
7329
  compiledIntent.bundle,
6817
- finalStep?.bindingDependencies
7330
+ finalStep?.bindingDependencies,
7331
+ planReport
6818
7332
  );
6819
7333
  }
6820
7334
  return this.createNqlStatementBundle(
@@ -6822,7 +7336,8 @@ var NqlBuilderImpl = class {
6822
7336
  compiledIntent.bundle.bindings ?? /* @__PURE__ */ new Map(),
6823
7337
  compiledIntent.bundle.runtimeBindings ?? /* @__PURE__ */ new Map(),
6824
7338
  compiledIntent.bundle,
6825
- finalStep?.bindingDependencies
7339
+ finalStep?.bindingDependencies,
7340
+ planReport
6826
7341
  );
6827
7342
  }
6828
7343
  runMutationStatement(adapter, bundle, intent, inTransaction) {
@@ -6911,18 +7426,28 @@ var NqlBuilderImpl = class {
6911
7426
  finalRows = rows.transformedRows;
6912
7427
  }
6913
7428
  } else if (step.final) {
7429
+ const planReport = isBindingFinalQuery(compiledIntent.bundle) && compiledIntent.kind === "query" ? createBindingFinalPlan(
7430
+ step.intent,
7431
+ sourceBundle,
7432
+ this.model,
7433
+ txAdapter.dialectCapabilities
7434
+ ) : void 0;
6914
7435
  const statementBundle = this.createNqlStatementBundle(
6915
7436
  { query: step.intent },
6916
7437
  priorBindings,
6917
7438
  runtimeBindings,
6918
7439
  sourceBundle,
6919
- step.bindingDependencies
7440
+ step.bindingDependencies,
7441
+ planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
6920
7442
  );
6921
7443
  const compiled = txAdapter.compile(
6922
7444
  statementBundle,
6923
7445
  this.nqlBundleCompileOptions()
6924
7446
  );
6925
7447
  finalRows = await txAdapter.execute(compiled);
7448
+ if (planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport)) {
7449
+ hydrateJsonAggIncludes(finalRows, planReport);
7450
+ }
6926
7451
  }
6927
7452
  if (step.bindName) {
6928
7453
  const boundQuery = sourceBundle.bindings?.get(step.bindName);
@@ -6967,12 +7492,19 @@ var NqlBuilderImpl = class {
6967
7492
  });
6968
7493
  }
6969
7494
  } else {
7495
+ const planReport = step.final && isBindingFinalQuery(compiledIntent.bundle) && compiledIntent.kind === "query" ? createBindingFinalPlan(
7496
+ step.intent,
7497
+ sourceBundle,
7498
+ this.model,
7499
+ adapter.dialectCapabilities
7500
+ ) : void 0;
6970
7501
  const statementBundle = this.createNqlStatementBundle(
6971
7502
  { query: step.intent },
6972
7503
  priorBindings,
6973
7504
  runtimeBindings,
6974
7505
  sourceBundle,
6975
- step.bindingDependencies
7506
+ step.bindingDependencies,
7507
+ planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
6976
7508
  );
6977
7509
  const compiled = adapter.compile(
6978
7510
  statementBundle,
@@ -6994,7 +7526,12 @@ var NqlBuilderImpl = class {
6994
7526
  }
6995
7527
  const finalStep = steps.at(-1);
6996
7528
  if (finalStep?.kind === "query") {
6997
- const planReport = isBindingFinalQuery(compiledIntent.bundle) ? createBindingFinalPlan(finalStep.intent) : this.planInternal();
7529
+ const planReport = isBindingFinalQuery(compiledIntent.bundle) ? createBindingFinalPlan(
7530
+ finalStep.intent,
7531
+ compiledIntent.bundle,
7532
+ this.model,
7533
+ this.adapter?.dialectCapabilities
7534
+ ) : this.planInternal();
6998
7535
  const dumpMeta2 = {
6999
7536
  compiledAt: /* @__PURE__ */ new Date(),
7000
7537
  ...this._schemaName !== void 0 && { schema: this._schemaName },
@@ -7070,11 +7607,24 @@ var NqlBuilderImpl = class {
7070
7607
  )).transformedRows;
7071
7608
  }
7072
7609
  if (isBindingFinalQuery(compiledIntent.bundle)) {
7610
+ const planReport2 = createBindingFinalPlan(
7611
+ compiledIntent.intent,
7612
+ compiledIntent.bundle,
7613
+ this.model,
7614
+ adapter.dialectCapabilities
7615
+ );
7073
7616
  const compiled2 = adapter.compile(
7074
- this.createFinalNqlStatementBundle(compiledIntent),
7617
+ this.createFinalNqlStatementBundle(
7618
+ compiledIntent,
7619
+ bindingFinalPlanHasJsonAggIncludes(planReport2) ? planReport2 : void 0
7620
+ ),
7075
7621
  this.nqlBundleCompileOptions()
7076
7622
  );
7077
- return adapter.execute(compiled2);
7623
+ const rows = await adapter.execute(compiled2);
7624
+ if (bindingFinalPlanHasJsonAggIncludes(planReport2)) {
7625
+ hydrateJsonAggIncludes(rows, planReport2);
7626
+ }
7627
+ return rows;
7078
7628
  }
7079
7629
  const planReport = this.planInternal();
7080
7630
  const finalBundle = this.createFinalNqlStatementBundle(compiledIntent);
@@ -7389,85 +7939,8 @@ async function cursorPaginate(builder, options) {
7389
7939
  };
7390
7940
  }
7391
7941
 
7392
- // src/dx/hydration-utils.ts
7393
- function hydrateJsonAggIncludes(results, planReport) {
7394
- const jsonAggDecisions = planReport.decisions.filter(
7395
- (d) => d.type === "include-strategy" && d.choice === "json_agg"
7396
- );
7397
- if (jsonAggDecisions.length === 0) {
7398
- return;
7399
- }
7400
- const relationInfo = /* @__PURE__ */ new Map();
7401
- for (const decision of jsonAggDecisions) {
7402
- const canonicalName = decision.context?.relation;
7403
- const includeAlias = decision.context?.includeAlias;
7404
- const relationType = decision.context?.relationType;
7405
- const isToOne = relationType === "belongsTo" || relationType === "hasOne";
7406
- if (typeof canonicalName === "string") {
7407
- relationInfo.set(canonicalName, {
7408
- isToOne,
7409
- includeAlias: typeof includeAlias === "string" ? includeAlias : canonicalName,
7410
- canonicalName
7411
- });
7412
- } else if (typeof includeAlias === "string") {
7413
- relationInfo.set(includeAlias, { isToOne, includeAlias });
7414
- }
7415
- }
7416
- if (relationInfo.size === 0) {
7417
- return;
7418
- }
7419
- for (const row of results) {
7420
- if (typeof row !== "object" || row === null) {
7421
- continue;
7422
- }
7423
- const record2 = row;
7424
- for (const [relationName, info] of relationInfo) {
7425
- const candidates = [relationName];
7426
- if (info.includeAlias && info.includeAlias !== relationName) {
7427
- candidates.push(info.includeAlias);
7428
- }
7429
- let actualColumnName = null;
7430
- for (const baseName of candidates) {
7431
- const snakeJson = `${baseName}_json`;
7432
- const camelJson = snakeJson.replace(
7433
- /_([a-z])/g,
7434
- (_, c) => c.toUpperCase()
7435
- );
7436
- if (snakeJson in record2) {
7437
- actualColumnName = snakeJson;
7438
- break;
7439
- }
7440
- if (camelJson in record2) {
7441
- actualColumnName = camelJson;
7442
- break;
7443
- }
7444
- }
7445
- if (actualColumnName) {
7446
- const jsonValue = record2[actualColumnName];
7447
- let parsed;
7448
- if (typeof jsonValue === "string") {
7449
- try {
7450
- parsed = JSON.parse(jsonValue);
7451
- } catch {
7452
- parsed = info.isToOne ? null : [];
7453
- }
7454
- } else if (Array.isArray(jsonValue)) {
7455
- parsed = jsonValue;
7456
- } else if (jsonValue === null || jsonValue === void 0) {
7457
- parsed = info.isToOne ? null : [];
7458
- } else {
7459
- parsed = jsonValue;
7460
- }
7461
- if (info.isToOne && Array.isArray(parsed)) {
7462
- parsed = parsed.length > 0 ? parsed[0] : null;
7463
- }
7464
- const outputKey = info.includeAlias ?? relationName;
7465
- record2[outputKey] = parsed;
7466
- delete record2[actualColumnName];
7467
- }
7468
- }
7469
- }
7470
- }
7942
+ // src/dx/result-hydrator.ts
7943
+ import { toColumnList as toColumnList3 } from "@dbsp/types";
7471
7944
 
7472
7945
  // src/dx/relation-paths.ts
7473
7946
  function nonEmptyString(value) {
@@ -7632,7 +8105,7 @@ var ResultHydrator = class {
7632
8105
  for (const includeInfo of subqueryIncludes) {
7633
8106
  const parentIds = [];
7634
8107
  for (const r of results) {
7635
- const id = this.extractKeyValue(
8108
+ const id = this.extractQueryKeyValue(
7636
8109
  r,
7637
8110
  includeInfo.sourceKey
7638
8111
  );
@@ -7844,14 +8317,16 @@ var ResultHydrator = class {
7844
8317
  * Get the foreign key column name from relation metadata.
7845
8318
  */
7846
8319
  getForeignKeyColumn(foreignKey) {
7847
- if (!foreignKey) {
8320
+ const columns = toColumnList3(foreignKey);
8321
+ if (columns.length === 0) {
7848
8322
  return "parent_id";
7849
8323
  }
7850
- if (typeof foreignKey === "string") {
7851
- return foreignKey;
8324
+ if (columns.length !== 1) {
8325
+ throw new Error(
8326
+ `Recursive include hydration requires a single-column self-referential foreign key; got ${JSON.stringify(columns)}.`
8327
+ );
7852
8328
  }
7853
- const first = foreignKey[0];
7854
- return first ?? "parent_id";
8329
+ return columns[0];
7855
8330
  }
7856
8331
  /**
7857
8332
  * Build traversal config for recursive CTE.
@@ -7943,10 +8418,14 @@ var ResultHydrator = class {
7943
8418
  * case (slower but collision-safe for any value content).
7944
8419
  */
7945
8420
  extractKeyValue(obj, key) {
7946
- if (typeof key === "string") {
7947
- return obj[key];
8421
+ const columns = toColumnList3(key);
8422
+ if (columns.length === 0) {
8423
+ return void 0;
7948
8424
  }
7949
- const values = key.map((k) => obj[k]);
8425
+ if (columns.length === 1) {
8426
+ return obj[columns[0]];
8427
+ }
8428
+ const values = columns.map((k) => obj[k]);
7950
8429
  if (values.some((v2) => v2 === void 0 || v2 === null)) {
7951
8430
  return void 0;
7952
8431
  }
@@ -7956,6 +8435,17 @@ var ResultHydrator = class {
7956
8435
  }
7957
8436
  return parts.join(COMPOSITE_KEY_SEP);
7958
8437
  }
8438
+ extractQueryKeyValue(obj, key) {
8439
+ const columns = toColumnList3(key);
8440
+ if (columns.length === 0) {
8441
+ return void 0;
8442
+ }
8443
+ if (columns.length === 1) {
8444
+ return obj[columns[0]];
8445
+ }
8446
+ const values = columns.map((k) => obj[k]);
8447
+ return values.some((v2) => v2 === void 0 || v2 === null) ? void 0 : values;
8448
+ }
7959
8449
  };
7960
8450
 
7961
8451
  // src/dx/set-operation-builder.ts
@@ -10429,13 +10919,18 @@ function mapRelationType2(kind) {
10429
10919
  return "belongsToMany";
10430
10920
  }
10431
10921
  }
10922
+ function isGeneratedTableWithConfig(table) {
10923
+ const columns = table.columns;
10924
+ return typeof columns === "object" && columns !== null && typeof columns.type !== "string";
10925
+ }
10432
10926
  function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
10433
10927
  const columns = [];
10434
10928
  const foreignKeys = [];
10435
10929
  const indexes = [];
10436
10930
  const primaryKeys = [];
10931
+ const tableColumns = isGeneratedTableWithConfig(genTable) ? genTable.columns : genTable;
10437
10932
  const indexedColumns = /* @__PURE__ */ new Set();
10438
- for (const [colName, colDef] of Object.entries(genTable)) {
10933
+ for (const [colName, colDef] of Object.entries(tableColumns)) {
10439
10934
  const col2 = {
10440
10935
  name: colName,
10441
10936
  type: generatedTypeToColumnType(colDef.type),
@@ -10488,7 +10983,7 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
10488
10983
  const fkColumn = fk.columns[0];
10489
10984
  const targetColumn = fk.references.columns[0];
10490
10985
  if (fk.references.table === tableName && fkColumn !== void 0 && targetColumn !== void 0) {
10491
- const colDef = genTable[fkColumn];
10986
+ const colDef = tableColumns[fkColumn];
10492
10987
  const inferredName = fkColumn.endsWith("Id") ? fkColumn.slice(0, -2) : "parent";
10493
10988
  const parentRole = colDef?.references?.parentRole ?? inferredName;
10494
10989
  const childRole = colDef?.references?.childRole ?? (parentRole === "parent" ? "children" : `${parentRole}s`);
@@ -10504,7 +10999,10 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
10504
10999
  }
10505
11000
  }
10506
11001
  let primaryKey;
10507
- if (primaryKeys.length > 0) {
11002
+ const configuredPrimaryKey = isGeneratedTableWithConfig(genTable) ? genTable.primaryKey : void 0;
11003
+ if (configuredPrimaryKey !== void 0) {
11004
+ primaryKey = configuredPrimaryKey;
11005
+ } else if (primaryKeys.length > 0) {
10508
11006
  primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : primaryKeys;
10509
11007
  } else {
10510
11008
  const fkColumns = foreignKeys.flatMap((fk) => fk.columns);
@@ -10515,6 +11013,27 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
10515
11013
  primaryKey = hasId ? "id" : void 0;
10516
11014
  }
10517
11015
  }
11016
+ if (isGeneratedTableWithConfig(genTable)) {
11017
+ for (const fk of genTable.foreignKeys ?? []) {
11018
+ const foreignKey = {
11019
+ columns: [...fk.columns],
11020
+ references: {
11021
+ table: fk.references.table,
11022
+ columns: [...fk.references.columns]
11023
+ }
11024
+ };
11025
+ if (fk.onDelete) foreignKey.onDelete = fk.onDelete;
11026
+ if (fk.onUpdate) foreignKey.onUpdate = fk.onUpdate;
11027
+ foreignKeys.push(foreignKey);
11028
+ }
11029
+ for (const index of genTable.indexes ?? []) {
11030
+ indexes.push({
11031
+ columns: [...index.columns],
11032
+ unique: index.unique ?? false,
11033
+ ...index.name !== void 0 && { name: index.name }
11034
+ });
11035
+ }
11036
+ }
10518
11037
  const frozenColumns = Object.freeze(columns);
10519
11038
  return Object.freeze({
10520
11039
  name: tableName,
@@ -10698,6 +11217,7 @@ var ForeignKeyReferenceSchema = v.object({
10698
11217
  parentRole: v.optional(v.string()),
10699
11218
  childRole: v.optional(v.string())
10700
11219
  });
11220
+ var StringOrStringArraySchema = v.union([v.string(), v.array(v.string())]);
10701
11221
  var ColumnDefinitionSchema = v.object({
10702
11222
  type: SchemaColumnTypeSchema,
10703
11223
  primaryKey: v.optional(v.boolean()),
@@ -10711,7 +11231,24 @@ var ColumnDefinitionSchema = v.object({
10711
11231
  var FlatTableDefinitionSchema = safeRecord(ColumnDefinitionSchema);
10712
11232
  var TableDefWithConfigSchema = v.object({
10713
11233
  columns: FlatTableDefinitionSchema,
10714
- primaryKey: v.array(v.string()),
11234
+ primaryKey: v.optional(StringOrStringArraySchema),
11235
+ foreignKeys: v.optional(
11236
+ v.array(
11237
+ v.object({
11238
+ columns: v.array(v.string()),
11239
+ references: v.object({
11240
+ table: v.string(),
11241
+ columns: v.array(v.string())
11242
+ }),
11243
+ onDelete: v.optional(
11244
+ v.picklist(["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"])
11245
+ ),
11246
+ onUpdate: v.optional(
11247
+ v.picklist(["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"])
11248
+ )
11249
+ })
11250
+ )
11251
+ ),
10715
11252
  indexes: v.optional(
10716
11253
  v.array(
10717
11254
  v.object({
@@ -10733,15 +11270,15 @@ var IncludeStrategySchema = v.optional(
10733
11270
  var BelongsToRelationSchema = v.object({
10734
11271
  kind: v.literal("belongsTo"),
10735
11272
  target: v.string(),
10736
- foreignKey: v.string(),
10737
- targetKey: v.optional(v.string()),
11273
+ foreignKey: StringOrStringArraySchema,
11274
+ targetKey: v.optional(StringOrStringArraySchema),
10738
11275
  includeStrategy: IncludeStrategySchema
10739
11276
  });
10740
11277
  var HasManyRelationSchema = v.object({
10741
11278
  kind: v.literal("hasMany"),
10742
11279
  target: v.string(),
10743
- foreignKey: v.string(),
10744
- sourceKey: v.optional(v.string()),
11280
+ foreignKey: StringOrStringArraySchema,
11281
+ sourceKey: v.optional(StringOrStringArraySchema),
10745
11282
  includeStrategy: IncludeStrategySchema
10746
11283
  });
10747
11284
  var ManyToManyRelationSchema = v.object({