@dbsp/core 1.5.0 → 1.7.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
@@ -1665,7 +1665,7 @@ function isManyToMany(rel) {
1665
1665
  }
1666
1666
 
1667
1667
  // src/planner.ts
1668
- import { toColumnList } from "@dbsp/types";
1668
+ import { resolveJsonAggOrderKey, toColumnList } from "@dbsp/types";
1669
1669
 
1670
1670
  // src/dx/errors.ts
1671
1671
  var ExecutionError = class _ExecutionError extends Error {
@@ -1803,31 +1803,14 @@ function levenshteinDistance(a, b) {
1803
1803
  const lastRow = matrix[b.length];
1804
1804
  return lastRow ? lastRow[a.length] ?? 0 : 0;
1805
1805
  }
1806
- var RelationNotFoundError = class _RelationNotFoundError extends Error {
1807
- name = "RelationNotFoundError";
1808
- /**
1809
- * The table where the relation was requested.
1810
- */
1806
+ var TableScopedError = class extends Error {
1811
1807
  table;
1812
- /**
1813
- * The relation name that was requested but not found.
1814
- */
1815
1808
  requested;
1816
- /**
1817
- * Available relations on this table.
1818
- */
1819
1809
  available;
1820
- /**
1821
- * Suggested relation name (fuzzy match), if any.
1822
- */
1823
1810
  suggestion;
1824
- /**
1825
- * Public-safe message (no schema enumeration).
1826
- */
1827
1811
  publicMessage;
1828
- constructor(opts) {
1812
+ constructor(opts, genericMessage) {
1829
1813
  const suggestion = findClosestMatch(opts.requested, opts.available);
1830
- const genericMessage = "Relation not found";
1831
1814
  super(genericMessage);
1832
1815
  this.table = opts.table;
1833
1816
  this.requested = opts.requested;
@@ -1836,6 +1819,12 @@ var RelationNotFoundError = class _RelationNotFoundError extends Error {
1836
1819
  if (suggestion !== void 0) {
1837
1820
  this.suggestion = suggestion;
1838
1821
  }
1822
+ }
1823
+ };
1824
+ var RelationNotFoundError = class _RelationNotFoundError extends TableScopedError {
1825
+ name = "RelationNotFoundError";
1826
+ constructor(opts) {
1827
+ super(opts, "Relation not found");
1839
1828
  Object.setPrototypeOf(this, _RelationNotFoundError.prototype);
1840
1829
  }
1841
1830
  };
@@ -1923,39 +1912,10 @@ var TableNotFoundError = class _TableNotFoundError extends Error {
1923
1912
  Object.setPrototypeOf(this, _TableNotFoundError.prototype);
1924
1913
  }
1925
1914
  };
1926
- var ColumnNotFoundError = class _ColumnNotFoundError extends Error {
1915
+ var ColumnNotFoundError = class _ColumnNotFoundError extends TableScopedError {
1927
1916
  name = "ColumnNotFoundError";
1928
- /**
1929
- * The table where the column was requested.
1930
- */
1931
- table;
1932
- /**
1933
- * The column name that was requested but not found.
1934
- */
1935
- requested;
1936
- /**
1937
- * Available columns on this table.
1938
- */
1939
- available;
1940
- /**
1941
- * Suggested column name (fuzzy match), if any.
1942
- */
1943
- suggestion;
1944
- /**
1945
- * Public-safe message (no schema enumeration).
1946
- */
1947
- publicMessage;
1948
1917
  constructor(opts) {
1949
- const suggestion = findClosestMatch(opts.requested, opts.available);
1950
- const genericMessage = "Column not found";
1951
- super(genericMessage);
1952
- this.table = opts.table;
1953
- this.requested = opts.requested;
1954
- this.available = opts.available;
1955
- this.publicMessage = genericMessage;
1956
- if (suggestion !== void 0) {
1957
- this.suggestion = suggestion;
1958
- }
1918
+ super(opts, "Column not found");
1959
1919
  Object.setPrototypeOf(this, _ColumnNotFoundError.prototype);
1960
1920
  }
1961
1921
  };
@@ -2410,12 +2370,6 @@ function isSubquerySelectedColumnNonNullable(existsIntent, sourceTable, model) {
2410
2370
  if (!column) return false;
2411
2371
  return !column.nullable;
2412
2372
  }
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
- }
2419
2373
  function optimizeInToExists(where, sourceTable, model, negated = false) {
2420
2374
  switch (where.kind) {
2421
2375
  case "in": {
@@ -2744,7 +2698,8 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2744
2698
  const explicitJoinType = includeStrategy === "join" ? include.join ?? cascadedJoinType : void 0;
2745
2699
  const includeDecisionId = generateDecisionId(state, "include-strategy");
2746
2700
  const parentKey = relation.type === "belongsTo" ? relation.targetKey : relation.sourceKey;
2747
- const targetOrder = resolveTargetJsonAggOrderKey(model, relation.target);
2701
+ const targetTable = model.getTable(relation.target);
2702
+ const targetOrder = targetTable ? resolveJsonAggOrderKey(targetTable) : void 0;
2748
2703
  state.decisions.push({
2749
2704
  id: includeDecisionId,
2750
2705
  type: "include-strategy",
@@ -2760,10 +2715,10 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2760
2715
  foreignKey: relation.foreignKey
2761
2716
  },
2762
2717
  ...parentKey !== void 0 && { parentKey },
2763
- ...targetOrder.columns.length > 0 && {
2764
- targetPrimaryKey: targetOrder.columns
2718
+ ...targetOrder && targetOrder.columns.length > 0 && {
2719
+ targetOrderKey: targetOrder.columns
2765
2720
  },
2766
- ...targetOrder.fallback && { orderByFallback: true }
2721
+ ...targetOrder?.fallback && { orderByFallback: true }
2767
2722
  },
2768
2723
  choice: includeStrategy,
2769
2724
  // Embed joinType so the adapter's join handler can use it directly
@@ -4616,6 +4571,32 @@ function createHookManager() {
4616
4571
  return new HookManagerImpl();
4617
4572
  }
4618
4573
 
4574
+ // src/dx/include-hints.ts
4575
+ function applyHintToIncludeRecursive(inc, relationHints) {
4576
+ if (inc.via !== void 0) {
4577
+ if (inc.include && inc.include.length > 0) {
4578
+ return {
4579
+ ...inc,
4580
+ include: inc.include.map(
4581
+ (nested) => applyHintToIncludeRecursive(nested, relationHints)
4582
+ )
4583
+ };
4584
+ }
4585
+ return inc;
4586
+ }
4587
+ const hint = relationHints[inc.relation];
4588
+ const result = hint ? { ...inc, via: hint } : inc;
4589
+ if (result.include && result.include.length > 0) {
4590
+ return {
4591
+ ...result,
4592
+ include: result.include.map(
4593
+ (nested) => applyHintToIncludeRecursive(nested, relationHints)
4594
+ )
4595
+ };
4596
+ }
4597
+ return result;
4598
+ }
4599
+
4619
4600
  // src/dx/subquery-builder.ts
4620
4601
  var SubqueryBuilder = class _SubqueryBuilder {
4621
4602
  _from;
@@ -5263,38 +5244,13 @@ var IntentBuilder = class _IntentBuilder {
5263
5244
  return intent;
5264
5245
  }
5265
5246
  const updatedIncludes = intent.include.map(
5266
- (inc) => this.applyHintToInclude(inc)
5247
+ (inc) => applyHintToIncludeRecursive(inc, this.relationHints)
5267
5248
  );
5268
5249
  return {
5269
5250
  ...intent,
5270
5251
  include: updatedIncludes
5271
5252
  };
5272
5253
  }
5273
- /**
5274
- * Apply relation hint to a single include (recursively).
5275
- */
5276
- applyHintToInclude(inc) {
5277
- if (inc.via !== void 0) {
5278
- if (inc.include && inc.include.length > 0) {
5279
- return {
5280
- ...inc,
5281
- include: inc.include.map((nested) => this.applyHintToInclude(nested))
5282
- };
5283
- }
5284
- return inc;
5285
- }
5286
- const hint = this.relationHints[inc.relation];
5287
- const result = hint ? { ...inc, via: hint } : inc;
5288
- if (result.include && result.include.length > 0) {
5289
- return {
5290
- ...result,
5291
- include: result.include.map(
5292
- (nested) => this.applyHintToInclude(nested)
5293
- )
5294
- };
5295
- }
5296
- return result;
5297
- }
5298
5254
  /**
5299
5255
  * Clone the current state for immutable builder pattern.
5300
5256
  */
@@ -6257,7 +6213,7 @@ import {
6257
6213
  NqlLexer,
6258
6214
  compile as nqlCompile
6259
6215
  } from "@dbsp/nql";
6260
- import { toColumnList as toColumnList2 } from "@dbsp/types";
6216
+ import { resolveJsonAggOrderKey as resolveJsonAggOrderKey2 } from "@dbsp/types";
6261
6217
  import {
6262
6218
  explainUnsupportedNqlBindingIncludeHop,
6263
6219
  getTrustedNqlRelationFilterFields,
@@ -6411,7 +6367,11 @@ function virtualRelationIncludeShape(relation) {
6411
6367
  type: relationType,
6412
6368
  foreignKey,
6413
6369
  source: relation.sourceTable,
6414
- target: relation.targetTable
6370
+ target: relation.targetTable,
6371
+ ...relation.through !== void 0 && { through: relation.through },
6372
+ ...relation.throughTargetColumn !== void 0 && {
6373
+ otherKey: relation.throughTargetColumn
6374
+ }
6415
6375
  };
6416
6376
  }
6417
6377
  var BINDING_INCLUDE_NODE_ONLY_RELATION = {
@@ -6457,7 +6417,8 @@ function trustedBindingRelationColumnIsAdmitted(column) {
6457
6417
  if (trusted.cardinality === "one") {
6458
6418
  return !isDotted || trusted.hops.length > 0;
6459
6419
  }
6460
- return trusted.cardinality === "many" && trusted.relationType === "hasMany" && !isDotted && trusted.hops.length === 0;
6420
+ const hasCompleteManyToManyProof = (trusted.relationType === "manyToMany" || trusted.relationType === "belongsToMany") && trusted.through !== void 0 && trusted.throughSourceColumn !== void 0 && trusted.throughTargetColumn !== void 0;
6421
+ return trusted.cardinality === "many" && (trusted.relationType === "hasMany" || hasCompleteManyToManyProof) && !isDotted && trusted.hops.length === 0;
6461
6422
  }
6462
6423
  function assertProvenBindingInclude(bindingName, include, provenRelations) {
6463
6424
  const proven = provenRelations.get(include.relation);
@@ -6519,7 +6480,8 @@ function createBindingIncludeDecision(intent, include, relation, index, model) {
6519
6480
  const relationType = relation.relationType;
6520
6481
  const foreignKey = relationType === "belongsTo" ? relation.sourceColumn : relation.targetColumn;
6521
6482
  const parentKey = relationType === "belongsTo" ? relation.targetColumn : relation.sourceColumn;
6522
- const targetOrder = resolveTargetJsonAggOrderKey2(model, relation.targetTable);
6483
+ const targetTable = model.getTable(relation.targetTable);
6484
+ const targetOrder = targetTable ? resolveJsonAggOrderKey2(targetTable) : void 0;
6523
6485
  return {
6524
6486
  id: `binding-include-${index}`,
6525
6487
  type: "include-strategy",
@@ -6530,10 +6492,10 @@ function createBindingIncludeDecision(intent, include, relation, index, model) {
6530
6492
  relationType,
6531
6493
  foreignKey,
6532
6494
  parentKey,
6533
- ...targetOrder.columns.length > 0 && {
6534
- targetPrimaryKey: targetOrder.columns
6495
+ ...targetOrder && targetOrder.columns.length > 0 && {
6496
+ targetOrderKey: targetOrder.columns
6535
6497
  },
6536
- ...targetOrder.fallback && { orderByFallback: true },
6498
+ ...targetOrder?.fallback && { orderByFallback: true },
6537
6499
  includeAlias: include.relation,
6538
6500
  intentPath: `include[${index}]`
6539
6501
  },
@@ -6549,12 +6511,6 @@ function parentKeyForRelation(relation) {
6549
6511
  if (relation.type === "belongsTo") return relation.targetKey;
6550
6512
  return relation.sourceKey;
6551
6513
  }
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
6514
  function assertSupportedBindingIncludeHop(bindingName, rootIncludeRelation, relationName, relation, include, reasonPrefix = "") {
6559
6515
  const unsupportedReason = explainUnsupportedNqlBindingIncludeHop(
6560
6516
  relationName,
@@ -6653,10 +6609,11 @@ function createBindingTailIncludeDecisions(bindingName, include, firstHopRelatio
6653
6609
  const baseContext = { ...decision.context };
6654
6610
  delete baseContext.foreignKey;
6655
6611
  delete baseContext.parentKey;
6656
- delete baseContext.targetPrimaryKey;
6612
+ delete baseContext.targetOrderKey;
6657
6613
  delete baseContext.orderByFallback;
6658
6614
  const parentKey = parentKeyForRelation(relation);
6659
- const targetOrder = resolveTargetJsonAggOrderKey2(model, relation.target);
6615
+ const targetTable = model.getTable(relation.target);
6616
+ const targetOrder = targetTable ? resolveJsonAggOrderKey2(targetTable) : void 0;
6660
6617
  return {
6661
6618
  ...decision,
6662
6619
  id: `binding-include-${includeIndex}-tail-${tailIndex}`,
@@ -6674,10 +6631,10 @@ function createBindingTailIncludeDecisions(bindingName, include, firstHopRelatio
6674
6631
  // Leave parentKey absent when RelationIR does not specify it so the
6675
6632
  // adapter applies the same defaultPkColumnName fallback as real-table includes.
6676
6633
  ...parentKey !== void 0 && { parentKey },
6677
- ...targetOrder.columns.length > 0 && {
6678
- targetPrimaryKey: targetOrder.columns
6634
+ ...targetOrder && targetOrder.columns.length > 0 && {
6635
+ targetOrderKey: targetOrder.columns
6679
6636
  },
6680
- ...targetOrder.fallback && { orderByFallback: true },
6637
+ ...targetOrder?.fallback && { orderByFallback: true },
6681
6638
  intentPath: rebaseBindingTailIntentPath(
6682
6639
  decision.context.intentPath,
6683
6640
  includeIndex
@@ -6698,6 +6655,13 @@ function createBindingFinalPlan(intent, bundle, model, dialectCapabilities) {
6698
6655
  );
6699
6656
  }
6700
6657
  const proven = provenIncludes.get(include.relation);
6658
+ if (!proven) {
6659
+ throw bindingFinalIncludeError(
6660
+ intent.from,
6661
+ include.relation,
6662
+ "include relation proof was not emitted by the binding compiler"
6663
+ );
6664
+ }
6701
6665
  const firstHopDecision = createBindingIncludeDecision(
6702
6666
  intent,
6703
6667
  include,
@@ -6848,6 +6812,14 @@ function assembleNqlTemplate(strings, values) {
6848
6812
  function hasMutationBindingBodies(bundle) {
6849
6813
  return (bundle.mutationBindings?.size ?? 0) > 0;
6850
6814
  }
6815
+ function hasSnapshotReadBindingSteps(bundle) {
6816
+ return bundle.nqlProgramSequence?.some(
6817
+ (step) => step.kind === "query" && step.snapshot === true
6818
+ ) ?? false;
6819
+ }
6820
+ function hasExecutableNqlProgramSequence(bundle) {
6821
+ return hasMutationBindingBodies(bundle) || hasSnapshotReadBindingSteps(bundle);
6822
+ }
6851
6823
  function requireMutationBindingColumns(bundle, bindName) {
6852
6824
  const columns = bundle.bindingOutputSchemas?.get(bindName)?.columns;
6853
6825
  if (columns === void 0 || columns.length === 0) {
@@ -6890,11 +6862,13 @@ function toRuntimeBindingRow(bindName, row, columns, dbCasing) {
6890
6862
  }
6891
6863
  function createRuntimeBinding(bundle, bindName, rows, dbCasing) {
6892
6864
  const columns = requireMutationBindingColumns(bundle, bindName);
6865
+ const columnTypes = bundle.bindingOutputSchemas?.get(bindName)?.columnTypes;
6893
6866
  return {
6894
6867
  columns,
6895
6868
  rows: rows.map(
6896
6869
  (row) => toRuntimeBindingRow(bindName, row, columns, dbCasing)
6897
- )
6870
+ ),
6871
+ ...columnTypes !== void 0 && { columnTypes }
6898
6872
  };
6899
6873
  }
6900
6874
  function bindingEntryIsFinal(compiledIntent, bindName, boundQuery, sourceBundle) {
@@ -6911,6 +6885,7 @@ function orderedSequenceStepToProgramStep(step) {
6911
6885
  intent: step.query,
6912
6886
  ...step.bindName !== void 0 && { bindName: step.bindName },
6913
6887
  final: step.final,
6888
+ ...step.snapshot === true && { snapshot: true },
6914
6889
  ...dependencyStep.bindingDependencies !== void 0 && {
6915
6890
  bindingDependencies: dependencyStep.bindingDependencies
6916
6891
  }
@@ -6926,15 +6901,32 @@ function orderedSequenceStepToProgramStep(step) {
6926
6901
  }
6927
6902
  };
6928
6903
  }
6904
+ var structuredCloneFn = globalThis.structuredClone;
6905
+ function hasCustomSnapshotPrototype(value) {
6906
+ if (value === null || typeof value !== "object") return false;
6907
+ const prototype = Object.getPrototypeOf(value);
6908
+ return prototype !== Object.prototype && prototype !== null && prototype !== Array.prototype && prototype !== Date.prototype && prototype !== Map.prototype && prototype !== Set.prototype;
6909
+ }
6910
+ function cloneMutationSnapshotColumnValue(value) {
6911
+ if (hasCustomSnapshotPrototype(value) || structuredCloneFn === void 0) {
6912
+ return value;
6913
+ }
6914
+ try {
6915
+ return structuredCloneFn(value);
6916
+ } catch {
6917
+ return value;
6918
+ }
6919
+ }
6929
6920
  function snapshotMutationRows(rows) {
6930
6921
  return rows.map((row) => {
6931
- if (Array.isArray(row)) {
6932
- return [...row];
6922
+ if (row === null || typeof row !== "object") {
6923
+ return cloneMutationSnapshotColumnValue(row);
6933
6924
  }
6934
- if (row !== null && typeof row === "object") {
6935
- return { ...row };
6925
+ const snapshot = { ...row };
6926
+ for (const key of Reflect.ownKeys(snapshot)) {
6927
+ snapshot[key] = cloneMutationSnapshotColumnValue(snapshot[key]);
6936
6928
  }
6937
- return row;
6929
+ return snapshot;
6938
6930
  });
6939
6931
  }
6940
6932
  function assertNqlProgramSteps(compiledIntent, steps) {
@@ -6958,6 +6950,13 @@ function assertNqlProgramSteps(compiledIntent, steps) {
6958
6950
  }
6959
6951
  const seenBindNames = /* @__PURE__ */ new Set();
6960
6952
  for (const step of steps) {
6953
+ if (step.kind === "query" && step.snapshot === true) {
6954
+ if (step.bindName === void 0 || step.final) {
6955
+ throw new Error(
6956
+ "NQL program sequence query snapshots must be named non-final statements."
6957
+ );
6958
+ }
6959
+ }
6961
6960
  if (step.bindName === void 0) continue;
6962
6961
  if (seenBindNames.has(step.bindName)) {
6963
6962
  throw new Error(
@@ -7063,6 +7062,18 @@ function filterOptionalMapByBindingDependencies(source, bindingDependencies) {
7063
7062
  const filtered = filterMapByBindingDependencies(source, bindingDependencies);
7064
7063
  return filtered.size > 0 ? filtered : void 0;
7065
7064
  }
7065
+ function runtimeSourceBindingNames(sourceBundle) {
7066
+ const names = /* @__PURE__ */ new Set();
7067
+ for (const name of sourceBundle.mutationBindings?.keys() ?? []) {
7068
+ names.add(name);
7069
+ }
7070
+ for (const step of sourceBundle.nqlProgramSequence ?? []) {
7071
+ if (step.kind === "query" && step.snapshot === true && step.bindName !== void 0) {
7072
+ names.add(step.bindName);
7073
+ }
7074
+ }
7075
+ return names.size > 0 ? names : void 0;
7076
+ }
7066
7077
  function filterMapByRuntimeBindingDependencies(source, runtimeSourceBindings, bindingDependencies) {
7067
7078
  if (source === void 0) return /* @__PURE__ */ new Map();
7068
7079
  if (bindingDependencies === void 0 || runtimeSourceBindings === void 0 || runtimeSourceBindings.size === 0) {
@@ -7202,7 +7213,7 @@ var NqlBuilderImpl = class {
7202
7213
  }
7203
7214
  dump(meta) {
7204
7215
  const compiledIntent = this.compile();
7205
- if (hasMutationBindingBodies(compiledIntent.bundle)) {
7216
+ if (hasExecutableNqlProgramSequence(compiledIntent.bundle)) {
7206
7217
  return this.dumpNqlProgramSequence(compiledIntent, meta);
7207
7218
  }
7208
7219
  if (compiledIntent.kind === "mutation") {
@@ -7285,7 +7296,7 @@ var NqlBuilderImpl = class {
7285
7296
  createNqlStatementBundle(statement, bindings, runtimeBindings, sourceBundle, bindingDependencies, planReport) {
7286
7297
  const statementBindings = filterMapByRuntimeBindingDependencies(
7287
7298
  bindings,
7288
- sourceBundle.mutationBindings,
7299
+ runtimeSourceBindingNames(sourceBundle),
7289
7300
  bindingDependencies
7290
7301
  );
7291
7302
  const statementRuntimeBindings = filterMapByBindingDependencies(
@@ -7425,6 +7436,28 @@ var NqlBuilderImpl = class {
7425
7436
  if (step.final) {
7426
7437
  finalRows = rows.transformedRows;
7427
7438
  }
7439
+ } else if (step.snapshot === true && step.bindName !== void 0) {
7440
+ const statementBundle = this.createNqlStatementBundle(
7441
+ { query: step.intent },
7442
+ priorBindings,
7443
+ runtimeBindings,
7444
+ sourceBundle,
7445
+ step.bindingDependencies
7446
+ );
7447
+ const compiled = txAdapter.compile(
7448
+ statementBundle,
7449
+ this.nqlBundleCompileOptions()
7450
+ );
7451
+ const rows = await txAdapter.execute(compiled);
7452
+ runtimeBindings.set(
7453
+ step.bindName,
7454
+ createRuntimeBinding(
7455
+ sourceBundle,
7456
+ step.bindName,
7457
+ snapshotMutationRows(rows),
7458
+ txAdapter.dbCasing
7459
+ )
7460
+ );
7428
7461
  } else if (step.final) {
7429
7462
  const planReport = isBindingFinalQuery(compiledIntent.bundle) && compiledIntent.kind === "query" ? createBindingFinalPlan(
7430
7463
  step.intent,
@@ -7466,6 +7499,14 @@ var NqlBuilderImpl = class {
7466
7499
  const runtimeBindings = /* @__PURE__ */ new Map();
7467
7500
  const sequence = [];
7468
7501
  const steps = createNqlProgramSteps(compiledIntent);
7502
+ const dumpRuntimeBindingPreview = (bindName) => {
7503
+ const columnTypes = sourceBundle.bindingOutputSchemas?.get(bindName)?.columnTypes;
7504
+ return {
7505
+ columns: requireMutationBindingColumns(sourceBundle, bindName),
7506
+ rows: [],
7507
+ ...columnTypes !== void 0 && { columnTypes }
7508
+ };
7509
+ };
7469
7510
  for (const step of steps) {
7470
7511
  if (step.kind === "mutation") {
7471
7512
  const statementBundle = this.createNqlStatementBundle(
@@ -7486,10 +7527,10 @@ var NqlBuilderImpl = class {
7486
7527
  params: compiled.parameters
7487
7528
  });
7488
7529
  if (step.bindName !== void 0) {
7489
- runtimeBindings.set(step.bindName, {
7490
- columns: requireMutationBindingColumns(sourceBundle, step.bindName),
7491
- rows: []
7492
- });
7530
+ runtimeBindings.set(
7531
+ step.bindName,
7532
+ dumpRuntimeBindingPreview(step.bindName)
7533
+ );
7493
7534
  }
7494
7535
  } else {
7495
7536
  const planReport = step.final && isBindingFinalQuery(compiledIntent.bundle) && compiledIntent.kind === "query" ? createBindingFinalPlan(
@@ -7516,6 +7557,12 @@ var NqlBuilderImpl = class {
7516
7557
  sql: compiled.sql,
7517
7558
  params: compiled.parameters
7518
7559
  });
7560
+ if (step.snapshot === true && step.bindName !== void 0) {
7561
+ runtimeBindings.set(
7562
+ step.bindName,
7563
+ dumpRuntimeBindingPreview(step.bindName)
7564
+ );
7565
+ }
7519
7566
  }
7520
7567
  if (step.bindName !== void 0) {
7521
7568
  const boundQuery = sourceBundle.bindings?.get(step.bindName);
@@ -7595,7 +7642,7 @@ var NqlBuilderImpl = class {
7595
7642
  );
7596
7643
  }
7597
7644
  const compiledIntent = this.compile();
7598
- if (hasMutationBindingBodies(compiledIntent.bundle)) {
7645
+ if (hasExecutableNqlProgramSequence(compiledIntent.bundle)) {
7599
7646
  return this.executeNqlProgramSequence(compiledIntent, adapter);
7600
7647
  }
7601
7648
  if (compiledIntent.kind === "mutation") {
@@ -7940,7 +7987,7 @@ async function cursorPaginate(builder, options) {
7940
7987
  }
7941
7988
 
7942
7989
  // src/dx/result-hydrator.ts
7943
- import { toColumnList as toColumnList3 } from "@dbsp/types";
7990
+ import { toColumnList as toColumnList2 } from "@dbsp/types";
7944
7991
 
7945
7992
  // src/dx/relation-paths.ts
7946
7993
  function nonEmptyString(value) {
@@ -8317,7 +8364,7 @@ var ResultHydrator = class {
8317
8364
  * Get the foreign key column name from relation metadata.
8318
8365
  */
8319
8366
  getForeignKeyColumn(foreignKey) {
8320
- const columns = toColumnList3(foreignKey);
8367
+ const columns = toColumnList2(foreignKey);
8321
8368
  if (columns.length === 0) {
8322
8369
  return "parent_id";
8323
8370
  }
@@ -8418,7 +8465,7 @@ var ResultHydrator = class {
8418
8465
  * case (slower but collision-safe for any value content).
8419
8466
  */
8420
8467
  extractKeyValue(obj, key) {
8421
- const columns = toColumnList3(key);
8468
+ const columns = toColumnList2(key);
8422
8469
  if (columns.length === 0) {
8423
8470
  return void 0;
8424
8471
  }
@@ -8436,7 +8483,7 @@ var ResultHydrator = class {
8436
8483
  return parts.join(COMPOSITE_KEY_SEP);
8437
8484
  }
8438
8485
  extractQueryKeyValue(obj, key) {
8439
- const columns = toColumnList3(key);
8486
+ const columns = toColumnList2(key);
8440
8487
  if (columns.length === 0) {
8441
8488
  return void 0;
8442
8489
  }
@@ -9466,38 +9513,13 @@ var QueryBuilderImpl = class _QueryBuilderImpl {
9466
9513
  return intent;
9467
9514
  }
9468
9515
  const updatedIncludes = intent.include.map(
9469
- (inc) => this.applyHintToInclude(inc)
9516
+ (inc) => applyHintToIncludeRecursive(inc, this.relationHints)
9470
9517
  );
9471
9518
  return {
9472
9519
  ...intent,
9473
9520
  include: updatedIncludes
9474
9521
  };
9475
9522
  }
9476
- /**
9477
- * Apply relation hint to a single include (recursively).
9478
- */
9479
- applyHintToInclude(inc) {
9480
- if (inc.via !== void 0) {
9481
- if (inc.include && inc.include.length > 0) {
9482
- return {
9483
- ...inc,
9484
- include: inc.include.map((nested) => this.applyHintToInclude(nested))
9485
- };
9486
- }
9487
- return inc;
9488
- }
9489
- const hint = this.relationHints[inc.relation];
9490
- const result = hint ? { ...inc, via: hint } : inc;
9491
- if (result.include && result.include.length > 0) {
9492
- return {
9493
- ...result,
9494
- include: result.include.map(
9495
- (nested) => this.applyHintToInclude(nested)
9496
- )
9497
- };
9498
- }
9499
- return result;
9500
- }
9501
9523
  /**
9502
9524
  * Build the QueryIntent from current state.
9503
9525
  * Handles exactOptionalPropertyTypes by only including defined properties.
@@ -10867,6 +10889,7 @@ function rangeContainedBy(fieldOrColumn, valueOrRange, rangeType = "daterange")
10867
10889
  }
10868
10890
 
10869
10891
  // src/dx/schema-bridge.ts
10892
+ import { buildRelationKeyFields, toColumnList as toColumnList3 } from "@dbsp/types";
10870
10893
  import * as v from "valibot";
10871
10894
  function generatedTypeToColumnType(genType) {
10872
10895
  switch (genType) {
@@ -10919,6 +10942,37 @@ function mapRelationType2(kind) {
10919
10942
  return "belongsToMany";
10920
10943
  }
10921
10944
  }
10945
+ function sourceTableFromQualifiedName(qualifiedName) {
10946
+ const dotIndex = qualifiedName.indexOf(".");
10947
+ return dotIndex > 0 ? qualifiedName.slice(0, dotIndex) : qualifiedName;
10948
+ }
10949
+ function relationNameFromQualifiedName(qualifiedName) {
10950
+ const dotIndex = qualifiedName.indexOf(".");
10951
+ return dotIndex > 0 ? qualifiedName.slice(dotIndex + 1) : qualifiedName;
10952
+ }
10953
+ function columnListsEqual(left, right) {
10954
+ const leftColumns = toColumnList3(left);
10955
+ const rightColumns = toColumnList3(right);
10956
+ return leftColumns.length === rightColumns.length && leftColumns.every((column, index) => column === rightColumns[index]);
10957
+ }
10958
+ function generatedReferencedColumns(key) {
10959
+ const columns = toColumnList3(key);
10960
+ return columns.length > 0 ? columns : ["id"];
10961
+ }
10962
+ function findRelationForeignKey(sourceTable, genRelation, tables) {
10963
+ switch (genRelation.kind) {
10964
+ case "belongsTo":
10965
+ return tables.get(sourceTable)?.foreignKeys.find(
10966
+ (fk) => fk.references.table === genRelation.target && columnListsEqual(fk.columns, genRelation.foreignKey)
10967
+ );
10968
+ case "hasMany":
10969
+ return tables.get(genRelation.target)?.foreignKeys.find(
10970
+ (fk) => fk.references.table === sourceTable && columnListsEqual(fk.columns, genRelation.foreignKey)
10971
+ );
10972
+ case "manyToMany":
10973
+ return void 0;
10974
+ }
10975
+ }
10922
10976
  function isGeneratedTableWithConfig(table) {
10923
10977
  const columns = table.columns;
10924
10978
  return typeof columns === "object" && columns !== null && typeof columns.type !== "string";
@@ -11046,10 +11100,9 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
11046
11100
  }
11047
11101
  });
11048
11102
  }
11049
- function buildRelationIR(qualifiedName, genRelation, hints) {
11050
- const dotIndex = qualifiedName.indexOf(".");
11051
- const sourceTable = dotIndex > 0 ? qualifiedName.slice(0, dotIndex) : qualifiedName;
11052
- const relationName = dotIndex > 0 ? qualifiedName.slice(dotIndex + 1) : qualifiedName;
11103
+ function buildRelationIR(qualifiedName, genRelation, hints, relationFk) {
11104
+ const sourceTable = sourceTableFromQualifiedName(qualifiedName);
11105
+ const relationName = relationNameFromQualifiedName(qualifiedName);
11053
11106
  const hint = hints[qualifiedName];
11054
11107
  const relCardinality = genRelation.kind === "hasMany" ? genRelation.cardinality : void 0;
11055
11108
  const cardinality = hint?.cardinality === "one" ? "one" : relCardinality === "one" ? "one" : genRelation.kind === "belongsTo" ? "one" : "many";
@@ -11073,14 +11126,38 @@ function buildRelationIR(qualifiedName, genRelation, hints) {
11073
11126
  case "belongsTo":
11074
11127
  return {
11075
11128
  ...baseRelation,
11076
- foreignKey: genRelation.foreignKey,
11077
- ...genRelation.targetKey ? { targetKey: genRelation.targetKey } : {}
11129
+ ...buildRelationKeyFields(
11130
+ relationFk ?? {
11131
+ columns: genRelation.foreignKey,
11132
+ references: {
11133
+ table: genRelation.target,
11134
+ columns: generatedReferencedColumns(genRelation.targetKey)
11135
+ }
11136
+ },
11137
+ "belongsTo",
11138
+ {
11139
+ foreignKeyShape: genRelation.foreignKey,
11140
+ ...genRelation.targetKey !== void 0 ? { referencedKeyShape: genRelation.targetKey } : {}
11141
+ }
11142
+ )
11078
11143
  };
11079
11144
  case "hasMany":
11080
11145
  return {
11081
11146
  ...baseRelation,
11082
- foreignKey: genRelation.foreignKey,
11083
- ...genRelation.sourceKey ? { sourceKey: genRelation.sourceKey } : {}
11147
+ ...buildRelationKeyFields(
11148
+ relationFk ?? {
11149
+ columns: genRelation.foreignKey,
11150
+ references: {
11151
+ table: sourceTable,
11152
+ columns: generatedReferencedColumns(genRelation.sourceKey)
11153
+ }
11154
+ },
11155
+ "inverse",
11156
+ {
11157
+ foreignKeyShape: genRelation.foreignKey,
11158
+ ...genRelation.sourceKey !== void 0 ? { referencedKeyShape: genRelation.sourceKey } : {}
11159
+ }
11160
+ )
11084
11161
  };
11085
11162
  case "manyToMany":
11086
11163
  return {
@@ -11102,9 +11179,15 @@ function buildModelFromSchema(schema2) {
11102
11179
  );
11103
11180
  }
11104
11181
  for (const [qualifiedName, genRelation] of Object.entries(schema2.relations)) {
11182
+ const sourceTable = sourceTableFromQualifiedName(qualifiedName);
11105
11183
  relations.set(
11106
11184
  qualifiedName,
11107
- buildRelationIR(qualifiedName, genRelation, schema2.hints)
11185
+ buildRelationIR(
11186
+ qualifiedName,
11187
+ genRelation,
11188
+ schema2.hints,
11189
+ findRelationForeignKey(sourceTable, genRelation, tables)
11190
+ )
11108
11191
  );
11109
11192
  }
11110
11193
  return new ModelIRImpl(tables, relations);