@dbsp/core 1.0.5 → 1.2.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
@@ -2601,7 +2601,8 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2601
2601
  return;
2602
2602
  }
2603
2603
  const includePath = `${sourceTable}.${relation.name}`;
2604
- if (state.visitedIncludes.has(includePath)) {
2604
+ const isSelfReferentialRelation = relation.source === relation.target;
2605
+ if (!isSelfReferentialRelation && state.visitedIncludes.has(includePath)) {
2605
2606
  state.warnings.push({
2606
2607
  code: "CIRCULAR_INCLUDE",
2607
2608
  message: `Circular include detected: ${includePath}`,
@@ -2609,12 +2610,12 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2609
2610
  });
2610
2611
  return;
2611
2612
  }
2612
- state.visitedIncludes.add(includePath);
2613
+ if (!isSelfReferentialRelation) state.visitedIncludes.add(includePath);
2613
2614
  const relationPath = `${sourceTable}.${relation.name}`;
2614
2615
  const paths = state.relationAccessCounts.get(relationPath) ?? [];
2615
2616
  paths.push(intentPath);
2616
2617
  state.relationAccessCounts.set(relationPath, paths);
2617
- const isRecursiveInclude = (!!include.recursive || !!relation.recursive) && relation.source === relation.target;
2618
+ const isRecursiveInclude = (!!include.recursive || !!relation.recursive) && isSelfReferentialRelation;
2618
2619
  let includeStrategy;
2619
2620
  if (isRecursiveInclude) {
2620
2621
  if (opts.dialectCapabilities?.supportsRecursiveCTE === false) {
@@ -2749,7 +2750,7 @@ function processInclude(include, sourceTable, model, state, opts, intentPath, de
2749
2750
  }
2750
2751
  }
2751
2752
  }
2752
- state.visitedIncludes.delete(includePath);
2753
+ if (!isSelfReferentialRelation) state.visitedIncludes.delete(includePath);
2753
2754
  }
2754
2755
  function disambiguateRelation(relationName, sourceTable, model, state, opts, _intentPath, viaHint) {
2755
2756
  const directRelation = model.getRelation(`${sourceTable}.${relationName}`);
@@ -4105,6 +4106,12 @@ function coalesce(fields, as) {
4105
4106
  };
4106
4107
  }
4107
4108
  function raw(sqlFragment, as) {
4109
+ if (typeof as !== "string" || as.trim().length === 0) {
4110
+ throw new InvalidOperationError(
4111
+ "raw",
4112
+ "raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
4113
+ );
4114
+ }
4108
4115
  validateIdentifier(as, "column");
4109
4116
  return {
4110
4117
  __expr: true,
@@ -5446,6 +5453,122 @@ var LightweightModelIR = class {
5446
5453
  };
5447
5454
 
5448
5455
  // src/dx/mutation-builders.ts
5456
+ function compileMutationIntent(adapter, intent, options) {
5457
+ switch (intent.type) {
5458
+ case "insert":
5459
+ return adapter.compileInsert(intent, options);
5460
+ case "insert_from":
5461
+ return adapter.compileInsertFrom(intent, options);
5462
+ case "update":
5463
+ return adapter.compileUpdate(intent, options);
5464
+ case "batchUpdate":
5465
+ return adapter.compileBatchUpdate(intent, options);
5466
+ case "delete":
5467
+ return adapter.compileDelete(intent, options);
5468
+ case "upsert":
5469
+ return adapter.compileUpsert(intent, options);
5470
+ case "upsert_from":
5471
+ return adapter.compileUpsertFrom(intent, options);
5472
+ }
5473
+ const _exhaustive = intent;
5474
+ throw new Error(
5475
+ `Unsupported mutation type: ${_exhaustive.type}`
5476
+ );
5477
+ }
5478
+ async function runMutationWithHooks(opts) {
5479
+ if (!opts.hookStore || !hasHooks(opts.hookStore)) {
5480
+ const prepared = opts.prepare(opts.intent);
5481
+ return prepared.execute();
5482
+ }
5483
+ return withReentrancyGuard(
5484
+ opts.hookStore,
5485
+ (store) => runMutationWithHooksInner(opts, store)
5486
+ );
5487
+ }
5488
+ async function runMutationWithHooksInner(opts, store) {
5489
+ const { intent } = opts;
5490
+ const operation = intent.type;
5491
+ const startTime = Date.now();
5492
+ const { cardinality, data } = extractMutationIntentData(intent);
5493
+ let ctx = Object.freeze({
5494
+ table: opts.table,
5495
+ operation,
5496
+ intent,
5497
+ cardinality,
5498
+ data,
5499
+ ...opts.schemaName !== void 0 ? { schemaName: opts.schemaName } : {},
5500
+ ...opts.inTransaction ? { inTransaction: true } : {}
5501
+ });
5502
+ try {
5503
+ if (store.beforeMutation.length > 0) {
5504
+ ctx = await runBeforeMutationHooks(
5505
+ store.beforeMutation,
5506
+ ctx,
5507
+ opts.onHookError
5508
+ );
5509
+ }
5510
+ const prepared = opts.prepare(intent);
5511
+ const duration = Date.now() - startTime;
5512
+ const result = await prepared.execute();
5513
+ if (store.afterMutation.length > 0) {
5514
+ const afterCtx = Object.freeze({
5515
+ ...ctx,
5516
+ sql: prepared.sql,
5517
+ parameters: prepared.parameters,
5518
+ duration
5519
+ });
5520
+ const transformed = await runAfterMutationHooks(
5521
+ store.afterMutation,
5522
+ afterCtx,
5523
+ [...prepared.getAfterMutationResult?.(result) ?? []],
5524
+ opts.onHookError
5525
+ );
5526
+ if (prepared.returnAfterMutationResult) {
5527
+ return transformed;
5528
+ }
5529
+ }
5530
+ return result;
5531
+ } catch (error) {
5532
+ if (store.onError.length > 0) {
5533
+ const errorCtx = {
5534
+ table: opts.table,
5535
+ operation,
5536
+ error,
5537
+ intent,
5538
+ phase: "beforeMutation",
5539
+ ...opts.schemaName !== void 0 ? { schemaName: opts.schemaName } : {}
5540
+ };
5541
+ const transformed = await runOnErrorHooks(store.onError, errorCtx);
5542
+ throw transformed;
5543
+ }
5544
+ throw error;
5545
+ }
5546
+ }
5547
+ function extractMutationIntentData(intent) {
5548
+ if (intent.type === "insert" || intent.type === "upsert") {
5549
+ const values = intent.values;
5550
+ return {
5551
+ cardinality: values.length > 1 ? "bulk" : "single",
5552
+ data: values.length > 1 ? values : values[0]
5553
+ };
5554
+ }
5555
+ if (intent.type === "update") {
5556
+ return {
5557
+ cardinality: "single",
5558
+ data: intent.set
5559
+ };
5560
+ }
5561
+ if (intent.type === "batchUpdate") {
5562
+ return {
5563
+ cardinality: "bulk",
5564
+ data: intent.updates
5565
+ };
5566
+ }
5567
+ if (intent.type === "insert_from" || intent.type === "upsert_from") {
5568
+ return { cardinality: "bulk", data: void 0 };
5569
+ }
5570
+ return { cardinality: "single", data: void 0 };
5571
+ }
5449
5572
  var MutationBuilderBase = class {
5450
5573
  table;
5451
5574
  model;
@@ -5500,11 +5623,15 @@ var MutationBuilderBase = class {
5500
5623
  Object.keys(compileOptions).length > 0 ? compileOptions : void 0
5501
5624
  );
5502
5625
  const meta = {
5503
- compiledAt: /* @__PURE__ */ new Date()
5626
+ compiledAt: /* @__PURE__ */ new Date(),
5627
+ ...this.schemaName !== void 0 && { schema: this.schemaName },
5628
+ ...extraOptions?.queryName !== void 0 && {
5629
+ queryName: extraOptions.queryName
5630
+ },
5631
+ ...extraOptions?.correlationId !== void 0 && {
5632
+ correlationId: extraOptions.correlationId
5633
+ }
5504
5634
  };
5505
- if (this.schemaName !== void 0) {
5506
- meta.schema = this.schemaName;
5507
- }
5508
5635
  return {
5509
5636
  sql: compiled.sql,
5510
5637
  parameters: compiled.parameters,
@@ -5514,129 +5641,38 @@ var MutationBuilderBase = class {
5514
5641
  }
5515
5642
  async execute() {
5516
5643
  const adapter = this.requireAdapter(this.operationName);
5517
- if (!this.hookStore || !hasHooks(this.hookStore)) {
5518
- return this.executeWithoutHooks(adapter);
5519
- }
5520
- return this.executeWithHooks(adapter);
5521
- }
5522
- async executeWithoutHooks(adapter) {
5523
5644
  const intent = this.buildIntent();
5524
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
5525
- const compiled = this.compileIntent(adapter, intent, compileOptions);
5526
- if (this.returningColumns && this.returningColumns.length > 0) {
5527
- const result = await adapter.execute(compiled);
5528
- return result;
5529
- }
5530
- await adapter.execute(compiled);
5531
- return void 0;
5532
- }
5533
- async executeWithHooks(adapter) {
5534
- const store = this.hookStore;
5535
- if (!store) throw new Error("executeWithHooks called without hookStore");
5536
- return withReentrancyGuard(
5537
- store,
5538
- (s) => this.executeWithHooksInner(adapter, s)
5539
- );
5540
- }
5541
- async executeWithHooksInner(adapter, store) {
5542
- const intent = this.buildIntent();
5543
- const operation = intent.type;
5544
- const startTime = Date.now();
5545
- const { cardinality, data } = this.extractIntentData(intent);
5546
- let ctx = Object.freeze({
5645
+ return runMutationWithHooks({
5547
5646
  table: this.table,
5548
- operation,
5549
5647
  intent,
5550
- cardinality,
5551
- data,
5552
- ...this.schemaName !== void 0 ? { schemaName: this.schemaName } : {},
5553
- ...this.inTransaction ? { inTransaction: true } : {}
5648
+ hookStore: this.hookStore,
5649
+ onHookError: this.onHookError,
5650
+ schemaName: this.schemaName,
5651
+ inTransaction: this.inTransaction,
5652
+ prepare: (preparedIntent) => this.prepareMutationExecution(adapter, preparedIntent)
5554
5653
  });
5555
- try {
5556
- if (store.beforeMutation.length > 0) {
5557
- ctx = await runBeforeMutationHooks(
5558
- store.beforeMutation,
5559
- ctx,
5560
- this.onHookError
5561
- );
5562
- }
5563
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
5564
- const compiled = this.compileIntent(adapter, intent, compileOptions);
5565
- const duration = Date.now() - startTime;
5566
- if (this.returningColumns && this.returningColumns.length > 0) {
5567
- const result = await adapter.execute(compiled);
5568
- const afterCtx = Object.freeze({
5569
- ...ctx,
5570
- sql: compiled.sql,
5571
- parameters: compiled.parameters,
5572
- duration
5573
- });
5574
- if (store.afterMutation.length > 0) {
5575
- const transformed = await runAfterMutationHooks(
5576
- store.afterMutation,
5577
- afterCtx,
5578
- result,
5579
- this.onHookError
5580
- );
5581
- return transformed;
5582
- }
5583
- return result;
5584
- }
5585
- await adapter.execute(compiled);
5586
- if (store.afterMutation.length > 0) {
5587
- const afterCtx = Object.freeze({
5588
- ...ctx,
5589
- sql: compiled.sql,
5590
- parameters: compiled.parameters,
5591
- duration
5592
- });
5593
- await runAfterMutationHooks(
5594
- store.afterMutation,
5595
- afterCtx,
5596
- [],
5597
- this.onHookError
5598
- );
5599
- }
5600
- return void 0;
5601
- } catch (error) {
5602
- if (store.onError.length > 0) {
5603
- const errorCtx = {
5604
- table: this.table,
5605
- operation,
5606
- error,
5607
- intent,
5608
- phase: "beforeMutation",
5609
- ...this.schemaName !== void 0 ? { schemaName: this.schemaName } : {}
5610
- };
5611
- const transformed = await runOnErrorHooks(store.onError, errorCtx);
5612
- throw transformed;
5613
- }
5614
- throw error;
5615
- }
5616
5654
  }
5617
- /** Extract cardinality and data from mutation intent */
5618
- extractIntentData(intent) {
5619
- if (intent.type === "insert" || intent.type === "upsert") {
5620
- const values = intent.values;
5621
- return {
5622
- cardinality: values.length > 1 ? "bulk" : "single",
5623
- data: values.length > 1 ? values : values[0]
5624
- };
5625
- }
5626
- if (intent.type === "update") {
5627
- return {
5628
- cardinality: "single",
5629
- data: intent.set
5630
- };
5631
- }
5632
- if (intent.type === "batchUpdate") {
5633
- const updates = intent.updates;
5655
+ prepareMutationExecution(adapter, intent) {
5656
+ const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
5657
+ const compiled = this.compileIntent(adapter, intent, compileOptions);
5658
+ if (this.returningColumns && this.returningColumns.length > 0) {
5634
5659
  return {
5635
- cardinality: "bulk",
5636
- data: updates
5660
+ sql: compiled.sql,
5661
+ parameters: compiled.parameters,
5662
+ execute: () => adapter.execute(compiled),
5663
+ getAfterMutationResult: (result) => result,
5664
+ returnAfterMutationResult: true
5637
5665
  };
5638
5666
  }
5639
- return { cardinality: "single", data: void 0 };
5667
+ return {
5668
+ sql: compiled.sql,
5669
+ parameters: compiled.parameters,
5670
+ execute: async () => {
5671
+ await adapter.execute(compiled);
5672
+ return void 0;
5673
+ },
5674
+ getAfterMutationResult: () => []
5675
+ };
5640
5676
  }
5641
5677
  };
5642
5678
  var InsertBuilder = class _InsertBuilder extends MutationBuilderBase {
@@ -5696,7 +5732,7 @@ var InsertBuilder = class _InsertBuilder extends MutationBuilderBase {
5696
5732
  return intent;
5697
5733
  }
5698
5734
  compileIntent(adapter, intent, options) {
5699
- return adapter.compileInsert(intent, options);
5735
+ return compileMutationIntent(adapter, intent, options);
5700
5736
  }
5701
5737
  };
5702
5738
  var UpdateBuilder = class _UpdateBuilder extends MutationBuilderBase {
@@ -5851,10 +5887,7 @@ var UpdateBuilder = class _UpdateBuilder extends MutationBuilderBase {
5851
5887
  return intent;
5852
5888
  }
5853
5889
  compileIntent(adapter, intent, options) {
5854
- if (intent.type === "batchUpdate") {
5855
- return adapter.compileBatchUpdate(intent, options);
5856
- }
5857
- return adapter.compileUpdate(intent, options);
5890
+ return compileMutationIntent(adapter, intent, options);
5858
5891
  }
5859
5892
  };
5860
5893
  var DeleteBuilder = class _DeleteBuilder extends MutationBuilderBase {
@@ -5941,7 +5974,7 @@ var DeleteBuilder = class _DeleteBuilder extends MutationBuilderBase {
5941
5974
  return intent;
5942
5975
  }
5943
5976
  compileIntent(adapter, intent, options) {
5944
- return adapter.compileDelete(intent, options);
5977
+ return compileMutationIntent(adapter, intent, options);
5945
5978
  }
5946
5979
  };
5947
5980
  var UpsertBuilder = class _UpsertBuilder extends MutationBuilderBase {
@@ -6073,7 +6106,7 @@ var UpsertBuilder = class _UpsertBuilder extends MutationBuilderBase {
6073
6106
  return intent;
6074
6107
  }
6075
6108
  compileIntent(adapter, intent, options) {
6076
- return adapter.compileUpsert(intent, options);
6109
+ return compileMutationIntent(adapter, intent, options);
6077
6110
  }
6078
6111
  };
6079
6112
 
@@ -6126,80 +6159,192 @@ function negotiateFeatures(model, capabilities, behavior = "warning", checkers =
6126
6159
  }
6127
6160
 
6128
6161
  // src/dx/nql.ts
6129
- import { compile as nqlCompile } from "@dbsp/nql";
6130
- function toNqlLiteral(value, index) {
6131
- if (value === null) {
6132
- return "null";
6162
+ import {
6163
+ NqlLexer,
6164
+ compile as nqlCompile
6165
+ } from "@dbsp/nql";
6166
+ import { NQL_INTERNAL_COMPILER_OPTIONS } from "@dbsp/types/internal";
6167
+ var NQL_RAW_FRAGMENT = /* @__PURE__ */ Symbol("NqlRawFragment");
6168
+ function hasNqlBindings(bundle) {
6169
+ return (bundle.bindings?.size ?? 0) > 0;
6170
+ }
6171
+ function assertNoMutationBindingBodies(bundle) {
6172
+ const mutationBindings = bundle.mutationBindings;
6173
+ if (!mutationBindings || mutationBindings.size === 0) {
6174
+ return;
6133
6175
  }
6134
- switch (typeof value) {
6135
- case "boolean":
6136
- return value ? "true" : "false";
6137
- case "number": {
6138
- if (!Number.isFinite(value)) {
6139
- throw new Error(
6140
- `nql\`...\`: cannot interpolate non-finite number (${value}) at position ${index}. Only finite numbers are supported. For dynamic NQL structure use the builder API (orm.select()). See issue #134.`
6141
- );
6142
- }
6143
- const s = value < 0 ? `-${Math.abs(value)}` : String(value);
6144
- if (/[eE]/.test(s)) {
6145
- throw new Error(
6146
- `nql\`...\`: number ${value} at position ${index} has no exact NQL numeric literal form (exponential notation). Convert it yourself or use the builder API (orm.select()). See issue #134.`
6147
- );
6148
- }
6149
- return s;
6176
+ const bindNames = [...mutationBindings.keys()].map((name) => `'${name}'`).join(", ");
6177
+ throw new Error(
6178
+ `NQL tag programs cannot use mutation bodies in | bind clauses (${bindNames}); writable binding CTEs are disabled for tag execution (#173). Use a read-only bind before a single final mutation, or execute the mutation explicitly.`
6179
+ );
6180
+ }
6181
+ function nqlRaw(fragment) {
6182
+ if (typeof fragment !== "string") {
6183
+ throw new TypeError("nqlRaw() expects a string fragment");
6184
+ }
6185
+ const raw2 = { fragment };
6186
+ Object.defineProperty(raw2, NQL_RAW_FRAGMENT, {
6187
+ value: true,
6188
+ enumerable: false
6189
+ });
6190
+ return Object.freeze(raw2);
6191
+ }
6192
+ function isNqlRawFragment(value) {
6193
+ if (typeof value !== "object" || value === null) {
6194
+ return false;
6195
+ }
6196
+ if (!Object.hasOwn(value, NQL_RAW_FRAGMENT)) {
6197
+ return false;
6198
+ }
6199
+ return value[NQL_RAW_FRAGMENT] === true;
6200
+ }
6201
+ function isInsideGeneratedRange(start, end, generatedRanges) {
6202
+ return generatedRanges.some(
6203
+ (range) => start >= range.start && end <= range.end
6204
+ );
6205
+ }
6206
+ function findOverlappingGeneratedRange(start, end, generatedRanges) {
6207
+ return generatedRanges.find(
6208
+ (range) => start < range.end && end > range.start
6209
+ );
6210
+ }
6211
+ function findExactGeneratedRange(name, start, end, generatedRanges) {
6212
+ return generatedRanges.find(
6213
+ (range) => range.name === name && range.start === start && range.end === end
6214
+ );
6215
+ }
6216
+ function generatedParamLexError(name) {
6217
+ return `Generated NQL parameter :${name} was not recognized as exactly one NamedParam token after raw-fragment assembly; check adjacent nqlRaw() fragments for quotes, comments, or identifier text that can swallow the placeholder.`;
6218
+ }
6219
+ function findInternalParamSourceError(query, generatedRanges) {
6220
+ const lexResult = NqlLexer.tokenize(query);
6221
+ const generatedTokenCounts = new Map(
6222
+ generatedRanges.map((range) => [range.name, 0])
6223
+ );
6224
+ for (const token of lexResult.tokens) {
6225
+ if (token.tokenType.name !== "NamedParam") {
6226
+ continue;
6150
6227
  }
6151
- case "string": {
6152
- if (value.includes("\n") || value.includes("\r")) {
6153
- throw new Error(
6154
- `nql\`...\`: cannot interpolate a string containing a newline at position ${index}. NQL string literals do not support raw newline characters. For dynamic NQL structure use the builder API (orm.select()). See issue #134.`
6155
- );
6156
- }
6157
- const escaped = value.replaceAll("'", "''");
6158
- return `'${escaped}'`;
6228
+ const name = token.image.slice(1);
6229
+ if (!name.startsWith("__p")) {
6230
+ continue;
6159
6231
  }
6160
- default: {
6161
- const typeName = value === void 0 ? "undefined" : typeof value;
6162
- throw new Error(
6163
- `nql\`...\`: cannot interpolate value of type "${typeName}" at position ${index}. Only string, number, boolean, and null are supported; for dynamic NQL fragments use the builder API (orm.select()). See issue #134.`
6232
+ const start = token.startOffset;
6233
+ const end = (token.endOffset ?? start + token.image.length - 1) + 1;
6234
+ const exactGeneratedRange = findExactGeneratedRange(
6235
+ name,
6236
+ start,
6237
+ end,
6238
+ generatedRanges
6239
+ );
6240
+ if (exactGeneratedRange) {
6241
+ generatedTokenCounts.set(
6242
+ exactGeneratedRange.name,
6243
+ (generatedTokenCounts.get(exactGeneratedRange.name) ?? 0) + 1
6164
6244
  );
6245
+ continue;
6246
+ }
6247
+ const overlappingGeneratedRange = findOverlappingGeneratedRange(
6248
+ start,
6249
+ end,
6250
+ generatedRanges
6251
+ );
6252
+ if (overlappingGeneratedRange) {
6253
+ return generatedParamLexError(overlappingGeneratedRange.name);
6254
+ }
6255
+ if (!isInsideGeneratedRange(start, end, generatedRanges)) {
6256
+ return `Reserved NQL parameter namespace "__p" cannot be referenced by user source (${token.image}).`;
6257
+ }
6258
+ }
6259
+ for (const range of generatedRanges) {
6260
+ if (generatedTokenCounts.get(range.name) !== 1) {
6261
+ return generatedParamLexError(range.name);
6165
6262
  }
6166
6263
  }
6264
+ return void 0;
6167
6265
  }
6168
- function createNqlTag(schemaDefinition, model, adapter, schemaName) {
6169
- return function nql(strings, ...values) {
6170
- let query = strings[0] ?? "";
6171
- for (let i = 0; i < values.length; i++) {
6172
- query += toNqlLiteral(values[i], i) + (strings[i + 1] ?? "");
6266
+ function assembleNqlTemplate(strings, values) {
6267
+ const params = /* @__PURE__ */ Object.create(null);
6268
+ const generatedRanges = [];
6269
+ let query = strings[0] ?? "";
6270
+ let boundIndex = 0;
6271
+ for (let i = 0; i < values.length; i++) {
6272
+ const value = values[i];
6273
+ if (isNqlRawFragment(value)) {
6274
+ query += value.fragment;
6275
+ } else {
6276
+ const name = `__p${boundIndex++}`;
6277
+ const placeholder = `:${name}`;
6278
+ const start = query.length;
6279
+ query += placeholder;
6280
+ generatedRanges.push({ name, start, end: start + placeholder.length });
6281
+ params[name] = value;
6173
6282
  }
6283
+ query += strings[i + 1] ?? "";
6284
+ }
6285
+ return {
6286
+ query,
6287
+ params,
6288
+ hasBoundParams: boundIndex > 0,
6289
+ sourceError: findInternalParamSourceError(query, generatedRanges)
6290
+ };
6291
+ }
6292
+ function createNqlTag(schemaDefinition, model, adapter, schemaName, hookStore, onHookError, inTransaction) {
6293
+ return function nql(strings, ...values) {
6294
+ const assembled = assembleNqlTemplate(strings, values);
6174
6295
  return new NqlBuilderImpl(
6175
- query,
6296
+ assembled.query,
6297
+ assembled.params,
6298
+ assembled.hasBoundParams,
6299
+ assembled.sourceError,
6176
6300
  schemaDefinition,
6177
6301
  model,
6178
6302
  adapter,
6179
- schemaName
6303
+ schemaName,
6304
+ hookStore,
6305
+ onHookError,
6306
+ inTransaction
6180
6307
  );
6181
6308
  };
6182
6309
  }
6183
6310
  var NqlBuilderImpl = class {
6184
- _intent;
6311
+ _compiled;
6185
6312
  query;
6313
+ params;
6314
+ hasBoundParams;
6315
+ sourceError;
6186
6316
  schemaDefinition;
6187
6317
  model;
6188
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Reserved for future schema-scoping support
6189
6318
  _schemaName;
6190
6319
  adapter;
6191
- constructor(query, schemaDefinition, model, adapter, schemaName) {
6320
+ hookStore;
6321
+ onHookError;
6322
+ inTransaction;
6323
+ constructor(query, params, hasBoundParams, sourceError, schemaDefinition, model, adapter, schemaName, hookStore, onHookError, inTransaction) {
6192
6324
  this.query = query;
6325
+ this.params = params;
6326
+ this.hasBoundParams = hasBoundParams;
6327
+ this.sourceError = sourceError;
6193
6328
  this.schemaDefinition = schemaDefinition;
6194
6329
  this.model = model;
6195
6330
  this.adapter = adapter;
6196
6331
  this._schemaName = schemaName;
6332
+ this.hookStore = hookStore;
6333
+ this.onHookError = onHookError;
6334
+ this.inTransaction = inTransaction;
6197
6335
  }
6198
6336
  compile() {
6199
- if (this._intent) {
6200
- return this._intent;
6337
+ if (this._compiled) {
6338
+ return this._compiled;
6339
+ }
6340
+ if (this.sourceError !== void 0) {
6341
+ throw new Error(`NQL compilation failed: ${this.sourceError}`);
6201
6342
  }
6202
- const compilerOptions = extractPseudoColumnKeywords(this.model);
6343
+ const compilerOptions = {
6344
+ ...extractPseudoColumnKeywords(this.model) ?? {},
6345
+ params: this.params,
6346
+ [NQL_INTERNAL_COMPILER_OPTIONS]: { allowInternalParams: true }
6347
+ };
6203
6348
  const result = nqlCompile(
6204
6349
  this.query,
6205
6350
  this.schemaDefinition,
@@ -6208,28 +6353,57 @@ var NqlBuilderImpl = class {
6208
6353
  );
6209
6354
  if (!result.success) {
6210
6355
  const errors = result.errors?.map((e) => e.message).join(", ") ?? "Unknown error";
6211
- throw new Error(`NQL compilation failed: ${errors}`);
6212
- }
6213
- if (result.ast?.mutation && !result.ast?.query) {
6214
- throw new Error(
6215
- "INSERT/UPDATE/DELETE/UPSERT not yet supported via the nql`...` tagged template. Use orm.insert(table, data) / orm.update(table, set) / orm.delete(table) / orm.upsert(table, data) instead. Tracking: https://github.com/oorabona/db-semantic-planner/issues/113"
6216
- );
6356
+ const rawHint = this.hasBoundParams ? " If an interpolation was intended as NQL structure, wrap a trusted fragment with nqlRaw()." : "";
6357
+ throw new Error(`NQL compilation failed: ${errors}${rawHint}`);
6217
6358
  }
6218
- if (!result.ast?.query) {
6359
+ const bundle = result.ast;
6360
+ if (!bundle) {
6219
6361
  throw new Error("NQL compilation failed: no query AST produced");
6220
6362
  }
6221
- this._intent = result.ast.query;
6222
- return this._intent;
6363
+ assertNoMutationBindingBodies(bundle);
6364
+ if (bundle.query) {
6365
+ this._compiled = {
6366
+ kind: "query",
6367
+ bundle,
6368
+ intent: bundle.query
6369
+ };
6370
+ return this._compiled;
6371
+ }
6372
+ if (bundle.mutation) {
6373
+ this._compiled = {
6374
+ kind: "mutation",
6375
+ bundle,
6376
+ intent: bundle.mutation
6377
+ };
6378
+ return this._compiled;
6379
+ }
6380
+ throw new Error("NQL compilation failed: no query AST produced");
6223
6381
  }
6224
6382
  toIntentIR() {
6225
- return this.compile();
6383
+ return this.compile().intent;
6384
+ }
6385
+ planInternal() {
6386
+ const compiled = this.compile();
6387
+ if (compiled.kind === "mutation") {
6388
+ throw new Error(
6389
+ "NQL mutations do not have execution plans; use dump() for SQL and parameters."
6390
+ );
6391
+ }
6392
+ return plan(compiled.intent, this.model);
6226
6393
  }
6227
6394
  plan() {
6228
- const intent = this.compile();
6229
- return plan(intent, this.model);
6395
+ return this.planInternal();
6230
6396
  }
6231
6397
  dump(meta) {
6232
- const planReport = this.plan();
6398
+ const compiledIntent = this.compile();
6399
+ if (compiledIntent.kind === "mutation") {
6400
+ return this.dumpMutation(
6401
+ compiledIntent.bundle,
6402
+ compiledIntent.intent,
6403
+ meta
6404
+ );
6405
+ }
6406
+ const planReport = this.planInternal();
6233
6407
  if (!this.adapter) {
6234
6408
  return {
6235
6409
  plan: planReport,
@@ -6238,7 +6412,10 @@ var NqlBuilderImpl = class {
6238
6412
  ...meta !== void 0 && { meta }
6239
6413
  };
6240
6414
  }
6241
- const compiled = this.adapter.compile(planReport);
6415
+ const compiled = hasNqlBindings(compiledIntent.bundle) ? this.adapter.compile(
6416
+ compiledIntent.bundle,
6417
+ this.nqlBundleCompileOptions()
6418
+ ) : this.adapter.compile(planReport);
6242
6419
  try {
6243
6420
  return this.adapter.createDump(planReport, compiled, meta);
6244
6421
  } catch (err) {
@@ -6266,15 +6443,88 @@ var NqlBuilderImpl = class {
6266
6443
  throw err;
6267
6444
  }
6268
6445
  }
6269
- async all() {
6446
+ requireAdapter(operation) {
6270
6447
  if (!this.adapter) {
6448
+ throw new ExecutionError({
6449
+ operation,
6450
+ reason: "Adapter not configured",
6451
+ fix: "Pass adapter option when creating ORM: createOrm({ model, adapter })"
6452
+ });
6453
+ }
6454
+ return this.adapter;
6455
+ }
6456
+ mutationCompileOptions(extraOptions) {
6457
+ return {
6458
+ model: this.model,
6459
+ ...this._schemaName !== void 0 && { schemaName: this._schemaName },
6460
+ ...extraOptions
6461
+ };
6462
+ }
6463
+ nqlBundleCompileOptions() {
6464
+ return {
6465
+ model: this.model,
6466
+ ...this._schemaName !== void 0 && { schemaName: this._schemaName }
6467
+ };
6468
+ }
6469
+ dumpMutation(bundle, intent, meta) {
6470
+ const adapter = this.requireAdapter("dump");
6471
+ const compiled = adapter.compile(bundle, this.mutationCompileOptions(meta));
6472
+ const dumpMeta = {
6473
+ compiledAt: /* @__PURE__ */ new Date(),
6474
+ ...this._schemaName !== void 0 && { schema: this._schemaName },
6475
+ ...meta?.queryName !== void 0 && { queryName: meta.queryName },
6476
+ ...meta?.correlationId !== void 0 && {
6477
+ correlationId: meta.correlationId
6478
+ }
6479
+ };
6480
+ return {
6481
+ sql: compiled.sql,
6482
+ parameters: compiled.parameters,
6483
+ intent,
6484
+ meta: dumpMeta
6485
+ };
6486
+ }
6487
+ async all() {
6488
+ const adapter = this.adapter;
6489
+ if (!adapter) {
6271
6490
  throw new Error(
6272
6491
  "Cannot execute query: no adapter configured. Pass an adapter to createOrm() or use .toIntentIR() / .plan() for debugging."
6273
6492
  );
6274
6493
  }
6275
- const planReport = this.plan();
6276
- const compiled = this.adapter.compile(planReport);
6277
- return this.adapter.execute(compiled);
6494
+ const compiledIntent = this.compile();
6495
+ if (compiledIntent.kind === "mutation") {
6496
+ const hasReturning = (compiledIntent.intent.returning?.length ?? 0) > 0;
6497
+ return runMutationWithHooks({
6498
+ table: compiledIntent.intent.table,
6499
+ intent: compiledIntent.intent,
6500
+ hookStore: this.hookStore,
6501
+ onHookError: this.onHookError,
6502
+ schemaName: this._schemaName,
6503
+ inTransaction: this.inTransaction,
6504
+ prepare: () => {
6505
+ const compiled2 = adapter.compile(
6506
+ compiledIntent.bundle,
6507
+ this.mutationCompileOptions()
6508
+ );
6509
+ return {
6510
+ sql: compiled2.sql,
6511
+ parameters: compiled2.parameters,
6512
+ execute: () => adapter.execute(compiled2),
6513
+ getAfterMutationResult: (result) => hasReturning ? result : [],
6514
+ returnAfterMutationResult: hasReturning
6515
+ };
6516
+ }
6517
+ });
6518
+ }
6519
+ const planReport = this.planInternal();
6520
+ const compiled = hasNqlBindings(compiledIntent.bundle) ? adapter.compile(
6521
+ compiledIntent.bundle,
6522
+ this.nqlBundleCompileOptions()
6523
+ ) : adapter.compile(planReport);
6524
+ return adapter.execute(compiled);
6525
+ }
6526
+ async run() {
6527
+ await this.all();
6278
6528
  }
6279
6529
  async first() {
6280
6530
  const rows = await this.all();
@@ -6661,8 +6911,152 @@ function hydrateJsonAggIncludes(results, planReport) {
6661
6911
  }
6662
6912
  }
6663
6913
 
6914
+ // src/dx/relation-paths.ts
6915
+ function nonEmptyString(value) {
6916
+ return typeof value === "string" && value.length > 0 ? value : void 0;
6917
+ }
6918
+ function includeNode(value) {
6919
+ return value !== null && typeof value === "object" ? value : void 0;
6920
+ }
6921
+ function parseIntentPathIndexes(intentPath) {
6922
+ const indexes = [];
6923
+ const indexPattern = /include\[(\d+)\]/g;
6924
+ let execResult = indexPattern.exec(intentPath);
6925
+ while (execResult !== null) {
6926
+ const rawIndex = execResult[1];
6927
+ if (rawIndex === void 0) break;
6928
+ indexes.push(parseInt(rawIndex, 10));
6929
+ execResult = indexPattern.exec(intentPath);
6930
+ }
6931
+ return indexes;
6932
+ }
6933
+ function deriveRelationPathFromIntentPath(includes, intentPath, fallbackRelation) {
6934
+ if (includes && intentPath) {
6935
+ let current = includes;
6936
+ const path = [];
6937
+ for (const index of parseIntentPathIndexes(intentPath)) {
6938
+ const item = includeNode(current[index]);
6939
+ if (!item) break;
6940
+ const segment = nonEmptyString(item.via) ?? nonEmptyString(item.relation);
6941
+ if (!segment) break;
6942
+ path.push(segment);
6943
+ current = Array.isArray(item.include) ? item.include : [];
6944
+ }
6945
+ if (path.length > 0) return path.join(".");
6946
+ }
6947
+ return nonEmptyString(fallbackRelation);
6948
+ }
6949
+ function countDistinctRelationPathsByName(usages) {
6950
+ const pathsByRelationName = /* @__PURE__ */ new Map();
6951
+ for (const usage of usages) {
6952
+ const relationName = nonEmptyString(usage.relationName);
6953
+ if (!relationName) continue;
6954
+ const relationPath = nonEmptyString(usage.relationPath) ?? relationName;
6955
+ const paths = pathsByRelationName.get(relationName) ?? /* @__PURE__ */ new Set();
6956
+ paths.add(relationPath);
6957
+ pathsByRelationName.set(relationName, paths);
6958
+ }
6959
+ const counts = /* @__PURE__ */ new Map();
6960
+ for (const [relationName, paths] of pathsByRelationName) {
6961
+ counts.set(relationName, paths.size);
6962
+ }
6963
+ return counts;
6964
+ }
6965
+
6664
6966
  // src/dx/result-hydrator.ts
6665
6967
  var COMPOSITE_KEY_SEP = "\0";
6968
+ function stringProp(source, key) {
6969
+ const value = source?.[key];
6970
+ return typeof value === "string" && value.length > 0 ? value : void 0;
6971
+ }
6972
+ function resolveJoinIncludePath(planReport, decision) {
6973
+ const context = decision.context;
6974
+ const decisionRecord = decision;
6975
+ const contextRecord = context;
6976
+ const explicitRelationPath = stringProp(decisionRecord, "relationPath") ?? stringProp(contextRecord, "relationPath");
6977
+ if (explicitRelationPath) return explicitRelationPath;
6978
+ const fallbackRelation = stringProp(contextRecord, "relation") ?? stringProp(contextRecord, "includeAlias") ?? stringProp(decisionRecord, "relationName");
6979
+ const intentPath = stringProp(contextRecord, "intentPath");
6980
+ return deriveRelationPathFromIntentPath(
6981
+ Array.isArray(planReport.intent?.include) ? planReport.intent.include : void 0,
6982
+ intentPath,
6983
+ fallbackRelation
6984
+ );
6985
+ }
6986
+ function lastRelationSegment(path) {
6987
+ const segments = path.split(".");
6988
+ return segments[segments.length - 1] ?? path;
6989
+ }
6990
+ function resolveJoinRelationName(decision, relationPath) {
6991
+ const context = decision.context;
6992
+ const decisionRecord = decision;
6993
+ return stringProp(context, "relation") ?? stringProp(context, "includeAlias") ?? stringProp(decisionRecord, "relationName") ?? lastRelationSegment(relationPath);
6994
+ }
6995
+ function collectJoinHydrationInfos(planReport) {
6996
+ const candidates = [];
6997
+ for (const decision of planReport.decisions) {
6998
+ if (decision.type !== "include-strategy" || decision.choice !== "join") {
6999
+ continue;
7000
+ }
7001
+ const relationPath = resolveJoinIncludePath(planReport, decision);
7002
+ if (!relationPath) continue;
7003
+ const relationName = resolveJoinRelationName(decision, relationPath);
7004
+ const decisionRecord = decision;
7005
+ const contextRecord = decision.context;
7006
+ const hydrationPrefix = stringProp(decisionRecord, "hydrationPrefix") ?? stringProp(contextRecord, "hydrationPrefix");
7007
+ candidates.push({
7008
+ relationName,
7009
+ relationPath,
7010
+ ...hydrationPrefix && { hydrationPrefix }
7011
+ });
7012
+ }
7013
+ const pathCountsByRelationName = countDistinctRelationPathsByName(candidates);
7014
+ const infosByPath = /* @__PURE__ */ new Map();
7015
+ for (const candidate of candidates) {
7016
+ if (infosByPath.has(candidate.relationPath)) continue;
7017
+ const usesFullPath = (pathCountsByRelationName.get(candidate.relationName) ?? 0) > 1;
7018
+ const keyPrefix = candidate.hydrationPrefix ?? (usesFullPath ? candidate.relationPath : candidate.relationName);
7019
+ if (!keyPrefix) continue;
7020
+ infosByPath.set(candidate.relationPath, {
7021
+ relationPath: candidate.relationPath,
7022
+ segments: candidate.relationPath.split("."),
7023
+ keyPrefix,
7024
+ keyPrefixWithDot: `${keyPrefix}.`
7025
+ });
7026
+ }
7027
+ return [...infosByPath.values()];
7028
+ }
7029
+ function findLongestJoinInfo(key, infos) {
7030
+ let best;
7031
+ for (const info of infos) {
7032
+ if (!key.startsWith(info.keyPrefixWithDot)) continue;
7033
+ if (!best || info.keyPrefixWithDot.length > best.keyPrefixWithDot.length) {
7034
+ best = info;
7035
+ }
7036
+ }
7037
+ return best;
7038
+ }
7039
+ function assignNestedValue(target, path, value) {
7040
+ let cursor = target;
7041
+ for (let i = 0; i < path.length - 1; i++) {
7042
+ const segment = path[i];
7043
+ if (!segment) return;
7044
+ const existing = cursor[segment];
7045
+ if (existing === null) {
7046
+ return;
7047
+ } else if (typeof existing !== "object" || Array.isArray(existing)) {
7048
+ cursor[segment] = {};
7049
+ }
7050
+ cursor = cursor[segment];
7051
+ }
7052
+ const leaf = path[path.length - 1];
7053
+ if (leaf) cursor[leaf] = value;
7054
+ }
7055
+ function assignColumnValue(target, columnPath, value) {
7056
+ const segments = columnPath.split(".").filter(Boolean);
7057
+ if (segments.length === 0) return;
7058
+ assignNestedValue(target, segments, value);
7059
+ }
6666
7060
  var ResultHydrator = class {
6667
7061
  model;
6668
7062
  from;
@@ -6739,51 +7133,42 @@ var ResultHydrator = class {
6739
7133
  * E2E-004: JOIN strategy for to-one relations returns columns like "author.id", "author.name".
6740
7134
  */
6741
7135
  hydrateJoinIncludes(results, planReport) {
6742
- const joinDecisions = planReport.decisions.filter(
6743
- (d) => d.type === "include-strategy" && d.choice === "join"
7136
+ const joinInfos = collectJoinHydrationInfos(planReport);
7137
+ if (joinInfos.length === 0) return;
7138
+ const infosByDepth = [...joinInfos].sort(
7139
+ (a, b) => a.segments.length - b.segments.length
6744
7140
  );
6745
- if (joinDecisions.length === 0) {
6746
- return;
6747
- }
6748
- const joinInfos = joinDecisions.map((d) => d.context?.relation).filter((r) => typeof r === "string").map((relation) => ({ relation, prefix: `${relation}.` }));
6749
- if (joinInfos.length === 0) {
6750
- return;
6751
- }
6752
- const joinInfosWithCache = joinInfos.map((info) => ({
6753
- ...info,
6754
- cachedKeys: null
6755
- }));
6756
- if (results.length > 0) {
6757
- const firstRow = results.find(
6758
- (r) => typeof r === "object" && r !== null
6759
- );
6760
- if (firstRow) {
6761
- const firstRecord = firstRow;
6762
- const allKeys = Object.keys(firstRecord);
6763
- for (const info of joinInfosWithCache) {
6764
- info.cachedKeys = allKeys.filter((k) => k.startsWith(info.prefix));
6765
- }
6766
- }
6767
- }
6768
7141
  for (const row of results) {
6769
7142
  if (typeof row !== "object" || row === null) {
6770
7143
  continue;
6771
7144
  }
6772
7145
  const record2 = row;
6773
- for (const info of joinInfosWithCache) {
6774
- const keys = info.cachedKeys && info.cachedKeys.length > 0 ? info.cachedKeys : Object.keys(record2).filter((k) => k.startsWith(info.prefix));
6775
- if (keys.length === 0) continue;
6776
- const nestedObj = {};
6777
- let allNull = true;
6778
- for (const key of keys) {
6779
- const val = record2[key];
6780
- if (val !== void 0) {
6781
- nestedObj[key.slice(info.prefix.length)] = val;
6782
- delete record2[key];
6783
- if (val !== null) allNull = false;
6784
- }
7146
+ const valuesByPath = /* @__PURE__ */ new Map();
7147
+ for (const key of Object.keys(record2)) {
7148
+ const info = findLongestJoinInfo(key, joinInfos);
7149
+ if (!info) continue;
7150
+ const value = record2[key];
7151
+ let entry = valuesByPath.get(info.relationPath);
7152
+ if (!entry) {
7153
+ entry = { values: {}, allNull: true };
7154
+ valuesByPath.set(info.relationPath, entry);
6785
7155
  }
6786
- record2[info.relation] = allNull ? null : nestedObj;
7156
+ assignColumnValue(
7157
+ entry.values,
7158
+ key.slice(info.keyPrefixWithDot.length),
7159
+ value
7160
+ );
7161
+ if (value !== null && value !== void 0) entry.allNull = false;
7162
+ delete record2[key];
7163
+ }
7164
+ for (const info of infosByDepth) {
7165
+ const entry = valuesByPath.get(info.relationPath);
7166
+ if (!entry) continue;
7167
+ assignNestedValue(
7168
+ record2,
7169
+ info.segments,
7170
+ entry.allNull ? null : entry.values
7171
+ );
6787
7172
  }
6788
7173
  }
6789
7174
  }
@@ -8914,7 +9299,10 @@ function createOrmInstance(model, strictMode, relationHints, adapter, schemaName
8914
9299
  schemaDefinition,
8915
9300
  model,
8916
9301
  adapter,
8917
- schemaName
9302
+ schemaName,
9303
+ hookStore,
9304
+ onHookError,
9305
+ inTransaction
8918
9306
  );
8919
9307
  const mutationOpts = {
8920
9308
  model,
@@ -11474,6 +11862,7 @@ export {
11474
11862
  composeBeforeMutationHooks,
11475
11863
  composeBeforeQueryHooks,
11476
11864
  composeOnErrorHooks,
11865
+ countDistinctRelationPathsByName,
11477
11866
  createDialectCapabilities,
11478
11867
  createHookManager,
11479
11868
  createNqlTag,
@@ -11486,6 +11875,7 @@ export {
11486
11875
  defineModel,
11487
11876
  defineSchema,
11488
11877
  denseRank,
11878
+ deriveRelationPathFromIntentPath,
11489
11879
  detectForeignKeys,
11490
11880
  detectManyToMany,
11491
11881
  distinct,
@@ -11588,6 +11978,7 @@ export {
11588
11978
  normalizeSchema,
11589
11979
  not,
11590
11980
  notExists,
11981
+ nqlRaw,
11591
11982
  objectToWhereIntent,
11592
11983
  op,
11593
11984
  or,