@dbsp/core 1.3.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.d.ts +43 -11
- package/dist/index.js +674 -124
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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(
|
|
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
|
|
2329
|
-
if (
|
|
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 (
|
|
2429
|
+
if (subSelect?.type !== "fields") return where;
|
|
2347
2430
|
const fields = "fields" in subSelect ? subSelect.fields : void 0;
|
|
2348
|
-
if (
|
|
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
|
|
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
|
|
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,7 +6257,98 @@ import {
|
|
|
6163
6257
|
NqlLexer,
|
|
6164
6258
|
compile as nqlCompile
|
|
6165
6259
|
} from "@dbsp/nql";
|
|
6166
|
-
import {
|
|
6260
|
+
import { toColumnList as toColumnList2 } from "@dbsp/types";
|
|
6261
|
+
import {
|
|
6262
|
+
explainUnsupportedNqlBindingIncludeHop,
|
|
6263
|
+
getTrustedNqlRelationFilterFields,
|
|
6264
|
+
NQL_INTERNAL_COMPILER_OPTIONS
|
|
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
|
|
6167
6352
|
var NQL_RAW_FRAGMENT = /* @__PURE__ */ Symbol("NqlRawFragment");
|
|
6168
6353
|
function hasNqlBindings(bundle) {
|
|
6169
6354
|
return (bundle.bindings?.size ?? 0) > 0;
|
|
@@ -6186,6 +6371,9 @@ function findBindingFinalRelationFilter(where) {
|
|
|
6186
6371
|
case "not":
|
|
6187
6372
|
return findBindingFinalRelationFilter(where.condition);
|
|
6188
6373
|
case "relationFilter":
|
|
6374
|
+
if (getTrustedNqlRelationFilterFields(where) !== void 0) {
|
|
6375
|
+
return void 0;
|
|
6376
|
+
}
|
|
6189
6377
|
return formatBindingRelationPath(where.relation);
|
|
6190
6378
|
case "exists":
|
|
6191
6379
|
case "notExists":
|
|
@@ -6197,21 +6385,340 @@ function findBindingFinalRelationFilter(where) {
|
|
|
6197
6385
|
return void 0;
|
|
6198
6386
|
}
|
|
6199
6387
|
}
|
|
6200
|
-
function
|
|
6201
|
-
|
|
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);
|
|
6500
|
+
const relationColumns = intent.select?.type === "expressions" ? intent.select.columns.flatMap((column) => {
|
|
6501
|
+
if (column.kind !== "relationColumn") return [];
|
|
6502
|
+
if (column.column === "*" && bindingIncludesCoverRelationPath(intent.include, column.relation)) {
|
|
6503
|
+
return [];
|
|
6504
|
+
}
|
|
6505
|
+
if (trustedBindingRelationColumnIsAdmitted(column)) {
|
|
6506
|
+
return [];
|
|
6507
|
+
}
|
|
6508
|
+
return [column.relation];
|
|
6509
|
+
}) : [];
|
|
6202
6510
|
const relationFilter = intent.where ? findBindingFinalRelationFilter(intent.where) : void 0;
|
|
6203
6511
|
const havingRelationFilter = intent.having ? findBindingFinalRelationFilter(intent.having) : void 0;
|
|
6204
|
-
if (
|
|
6512
|
+
if (relationColumns.length > 0 || (intent.joins?.length ?? 0) > 0 || relationFilter !== void 0 || havingRelationFilter !== void 0) {
|
|
6205
6513
|
throw new Error(
|
|
6206
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.`
|
|
6207
6515
|
);
|
|
6208
6516
|
}
|
|
6209
6517
|
}
|
|
6210
|
-
function
|
|
6211
|
-
|
|
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
|
+
});
|
|
6212
6719
|
return {
|
|
6213
6720
|
rootTable: intent.from,
|
|
6214
|
-
decisions
|
|
6721
|
+
decisions,
|
|
6215
6722
|
warnings: [],
|
|
6216
6723
|
ctes: [],
|
|
6217
6724
|
intent,
|
|
@@ -6222,6 +6729,11 @@ function createBindingFinalPlan(intent) {
|
|
|
6222
6729
|
}
|
|
6223
6730
|
};
|
|
6224
6731
|
}
|
|
6732
|
+
function bindingFinalPlanHasJsonAggIncludes(planReport) {
|
|
6733
|
+
return planReport.decisions.some(
|
|
6734
|
+
(decision) => decision.type === "include-strategy" && decision.choice === "json_agg"
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6225
6737
|
function nqlRaw(fragment) {
|
|
6226
6738
|
if (typeof fragment !== "string") {
|
|
6227
6739
|
throw new TypeError("nqlRaw() expects a string fragment");
|
|
@@ -6681,7 +7193,12 @@ var NqlBuilderImpl = class {
|
|
|
6681
7193
|
"NQL mutations do not have execution plans; use dump() for SQL and parameters."
|
|
6682
7194
|
);
|
|
6683
7195
|
}
|
|
6684
|
-
return isBindingFinalQuery(compiled.bundle) ? createBindingFinalPlan(
|
|
7196
|
+
return isBindingFinalQuery(compiled.bundle) ? createBindingFinalPlan(
|
|
7197
|
+
compiled.intent,
|
|
7198
|
+
compiled.bundle,
|
|
7199
|
+
this.model,
|
|
7200
|
+
this.adapter?.dialectCapabilities
|
|
7201
|
+
) : this.planInternal();
|
|
6685
7202
|
}
|
|
6686
7203
|
dump(meta) {
|
|
6687
7204
|
const compiledIntent = this.compile();
|
|
@@ -6696,7 +7213,12 @@ var NqlBuilderImpl = class {
|
|
|
6696
7213
|
);
|
|
6697
7214
|
}
|
|
6698
7215
|
const bindingFinalQuery = isBindingFinalQuery(compiledIntent.bundle);
|
|
6699
|
-
const planReport = bindingFinalQuery ? createBindingFinalPlan(
|
|
7216
|
+
const planReport = bindingFinalQuery ? createBindingFinalPlan(
|
|
7217
|
+
compiledIntent.intent,
|
|
7218
|
+
compiledIntent.bundle,
|
|
7219
|
+
this.model,
|
|
7220
|
+
this.adapter?.dialectCapabilities
|
|
7221
|
+
) : this.planInternal();
|
|
6700
7222
|
if (!this.adapter) {
|
|
6701
7223
|
return {
|
|
6702
7224
|
plan: planReport,
|
|
@@ -6705,7 +7227,10 @@ var NqlBuilderImpl = class {
|
|
|
6705
7227
|
...meta !== void 0 && { meta }
|
|
6706
7228
|
};
|
|
6707
7229
|
}
|
|
6708
|
-
const finalBundle = this.createFinalNqlStatementBundle(
|
|
7230
|
+
const finalBundle = this.createFinalNqlStatementBundle(
|
|
7231
|
+
compiledIntent,
|
|
7232
|
+
bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
|
|
7233
|
+
);
|
|
6709
7234
|
const compiled = bindingFinalQuery || hasNqlBindings(finalBundle) ? this.adapter.compile(finalBundle, this.nqlBundleCompileOptions()) : this.adapter.compile(planReport);
|
|
6710
7235
|
try {
|
|
6711
7236
|
return this.adapter.createDump(planReport, compiled, meta);
|
|
@@ -6757,7 +7282,7 @@ var NqlBuilderImpl = class {
|
|
|
6757
7282
|
...this._schemaName !== void 0 && { schemaName: this._schemaName }
|
|
6758
7283
|
};
|
|
6759
7284
|
}
|
|
6760
|
-
createNqlStatementBundle(statement, bindings, runtimeBindings, sourceBundle, bindingDependencies) {
|
|
7285
|
+
createNqlStatementBundle(statement, bindings, runtimeBindings, sourceBundle, bindingDependencies, planReport) {
|
|
6761
7286
|
const statementBindings = filterMapByRuntimeBindingDependencies(
|
|
6762
7287
|
bindings,
|
|
6763
7288
|
sourceBundle.mutationBindings,
|
|
@@ -6781,6 +7306,7 @@ var NqlBuilderImpl = class {
|
|
|
6781
7306
|
);
|
|
6782
7307
|
return {
|
|
6783
7308
|
...statement,
|
|
7309
|
+
...planReport !== void 0 && { plan: planReport },
|
|
6784
7310
|
...statementBindings.size > 0 && { bindings: statementBindings },
|
|
6785
7311
|
...statementBindingOutputSchemas !== void 0 && {
|
|
6786
7312
|
bindingOutputSchemas: statementBindingOutputSchemas
|
|
@@ -6793,7 +7319,7 @@ var NqlBuilderImpl = class {
|
|
|
6793
7319
|
}
|
|
6794
7320
|
};
|
|
6795
7321
|
}
|
|
6796
|
-
createFinalNqlStatementBundle(compiledIntent) {
|
|
7322
|
+
createFinalNqlStatementBundle(compiledIntent, planReport) {
|
|
6797
7323
|
const finalStep = createNqlProgramSteps(compiledIntent).at(-1);
|
|
6798
7324
|
if (compiledIntent.kind === "query") {
|
|
6799
7325
|
return this.createNqlStatementBundle(
|
|
@@ -6801,7 +7327,8 @@ var NqlBuilderImpl = class {
|
|
|
6801
7327
|
compiledIntent.bundle.bindings ?? /* @__PURE__ */ new Map(),
|
|
6802
7328
|
compiledIntent.bundle.runtimeBindings ?? /* @__PURE__ */ new Map(),
|
|
6803
7329
|
compiledIntent.bundle,
|
|
6804
|
-
finalStep?.bindingDependencies
|
|
7330
|
+
finalStep?.bindingDependencies,
|
|
7331
|
+
planReport
|
|
6805
7332
|
);
|
|
6806
7333
|
}
|
|
6807
7334
|
return this.createNqlStatementBundle(
|
|
@@ -6809,7 +7336,8 @@ var NqlBuilderImpl = class {
|
|
|
6809
7336
|
compiledIntent.bundle.bindings ?? /* @__PURE__ */ new Map(),
|
|
6810
7337
|
compiledIntent.bundle.runtimeBindings ?? /* @__PURE__ */ new Map(),
|
|
6811
7338
|
compiledIntent.bundle,
|
|
6812
|
-
finalStep?.bindingDependencies
|
|
7339
|
+
finalStep?.bindingDependencies,
|
|
7340
|
+
planReport
|
|
6813
7341
|
);
|
|
6814
7342
|
}
|
|
6815
7343
|
runMutationStatement(adapter, bundle, intent, inTransaction) {
|
|
@@ -6898,18 +7426,28 @@ var NqlBuilderImpl = class {
|
|
|
6898
7426
|
finalRows = rows.transformedRows;
|
|
6899
7427
|
}
|
|
6900
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;
|
|
6901
7435
|
const statementBundle = this.createNqlStatementBundle(
|
|
6902
7436
|
{ query: step.intent },
|
|
6903
7437
|
priorBindings,
|
|
6904
7438
|
runtimeBindings,
|
|
6905
7439
|
sourceBundle,
|
|
6906
|
-
step.bindingDependencies
|
|
7440
|
+
step.bindingDependencies,
|
|
7441
|
+
planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
|
|
6907
7442
|
);
|
|
6908
7443
|
const compiled = txAdapter.compile(
|
|
6909
7444
|
statementBundle,
|
|
6910
7445
|
this.nqlBundleCompileOptions()
|
|
6911
7446
|
);
|
|
6912
7447
|
finalRows = await txAdapter.execute(compiled);
|
|
7448
|
+
if (planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport)) {
|
|
7449
|
+
hydrateJsonAggIncludes(finalRows, planReport);
|
|
7450
|
+
}
|
|
6913
7451
|
}
|
|
6914
7452
|
if (step.bindName) {
|
|
6915
7453
|
const boundQuery = sourceBundle.bindings?.get(step.bindName);
|
|
@@ -6954,12 +7492,19 @@ var NqlBuilderImpl = class {
|
|
|
6954
7492
|
});
|
|
6955
7493
|
}
|
|
6956
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;
|
|
6957
7501
|
const statementBundle = this.createNqlStatementBundle(
|
|
6958
7502
|
{ query: step.intent },
|
|
6959
7503
|
priorBindings,
|
|
6960
7504
|
runtimeBindings,
|
|
6961
7505
|
sourceBundle,
|
|
6962
|
-
step.bindingDependencies
|
|
7506
|
+
step.bindingDependencies,
|
|
7507
|
+
planReport !== void 0 && bindingFinalPlanHasJsonAggIncludes(planReport) ? planReport : void 0
|
|
6963
7508
|
);
|
|
6964
7509
|
const compiled = adapter.compile(
|
|
6965
7510
|
statementBundle,
|
|
@@ -6981,7 +7526,12 @@ var NqlBuilderImpl = class {
|
|
|
6981
7526
|
}
|
|
6982
7527
|
const finalStep = steps.at(-1);
|
|
6983
7528
|
if (finalStep?.kind === "query") {
|
|
6984
|
-
const planReport = isBindingFinalQuery(compiledIntent.bundle) ? createBindingFinalPlan(
|
|
7529
|
+
const planReport = isBindingFinalQuery(compiledIntent.bundle) ? createBindingFinalPlan(
|
|
7530
|
+
finalStep.intent,
|
|
7531
|
+
compiledIntent.bundle,
|
|
7532
|
+
this.model,
|
|
7533
|
+
this.adapter?.dialectCapabilities
|
|
7534
|
+
) : this.planInternal();
|
|
6985
7535
|
const dumpMeta2 = {
|
|
6986
7536
|
compiledAt: /* @__PURE__ */ new Date(),
|
|
6987
7537
|
...this._schemaName !== void 0 && { schema: this._schemaName },
|
|
@@ -7057,11 +7607,24 @@ var NqlBuilderImpl = class {
|
|
|
7057
7607
|
)).transformedRows;
|
|
7058
7608
|
}
|
|
7059
7609
|
if (isBindingFinalQuery(compiledIntent.bundle)) {
|
|
7610
|
+
const planReport2 = createBindingFinalPlan(
|
|
7611
|
+
compiledIntent.intent,
|
|
7612
|
+
compiledIntent.bundle,
|
|
7613
|
+
this.model,
|
|
7614
|
+
adapter.dialectCapabilities
|
|
7615
|
+
);
|
|
7060
7616
|
const compiled2 = adapter.compile(
|
|
7061
|
-
this.createFinalNqlStatementBundle(
|
|
7617
|
+
this.createFinalNqlStatementBundle(
|
|
7618
|
+
compiledIntent,
|
|
7619
|
+
bindingFinalPlanHasJsonAggIncludes(planReport2) ? planReport2 : void 0
|
|
7620
|
+
),
|
|
7062
7621
|
this.nqlBundleCompileOptions()
|
|
7063
7622
|
);
|
|
7064
|
-
|
|
7623
|
+
const rows = await adapter.execute(compiled2);
|
|
7624
|
+
if (bindingFinalPlanHasJsonAggIncludes(planReport2)) {
|
|
7625
|
+
hydrateJsonAggIncludes(rows, planReport2);
|
|
7626
|
+
}
|
|
7627
|
+
return rows;
|
|
7065
7628
|
}
|
|
7066
7629
|
const planReport = this.planInternal();
|
|
7067
7630
|
const finalBundle = this.createFinalNqlStatementBundle(compiledIntent);
|
|
@@ -7376,85 +7939,8 @@ async function cursorPaginate(builder, options) {
|
|
|
7376
7939
|
};
|
|
7377
7940
|
}
|
|
7378
7941
|
|
|
7379
|
-
// src/dx/
|
|
7380
|
-
|
|
7381
|
-
const jsonAggDecisions = planReport.decisions.filter(
|
|
7382
|
-
(d) => d.type === "include-strategy" && d.choice === "json_agg"
|
|
7383
|
-
);
|
|
7384
|
-
if (jsonAggDecisions.length === 0) {
|
|
7385
|
-
return;
|
|
7386
|
-
}
|
|
7387
|
-
const relationInfo = /* @__PURE__ */ new Map();
|
|
7388
|
-
for (const decision of jsonAggDecisions) {
|
|
7389
|
-
const canonicalName = decision.context?.relation;
|
|
7390
|
-
const includeAlias = decision.context?.includeAlias;
|
|
7391
|
-
const relationType = decision.context?.relationType;
|
|
7392
|
-
const isToOne = relationType === "belongsTo" || relationType === "hasOne";
|
|
7393
|
-
if (typeof canonicalName === "string") {
|
|
7394
|
-
relationInfo.set(canonicalName, {
|
|
7395
|
-
isToOne,
|
|
7396
|
-
includeAlias: typeof includeAlias === "string" ? includeAlias : canonicalName,
|
|
7397
|
-
canonicalName
|
|
7398
|
-
});
|
|
7399
|
-
} else if (typeof includeAlias === "string") {
|
|
7400
|
-
relationInfo.set(includeAlias, { isToOne, includeAlias });
|
|
7401
|
-
}
|
|
7402
|
-
}
|
|
7403
|
-
if (relationInfo.size === 0) {
|
|
7404
|
-
return;
|
|
7405
|
-
}
|
|
7406
|
-
for (const row of results) {
|
|
7407
|
-
if (typeof row !== "object" || row === null) {
|
|
7408
|
-
continue;
|
|
7409
|
-
}
|
|
7410
|
-
const record2 = row;
|
|
7411
|
-
for (const [relationName, info] of relationInfo) {
|
|
7412
|
-
const candidates = [relationName];
|
|
7413
|
-
if (info.includeAlias && info.includeAlias !== relationName) {
|
|
7414
|
-
candidates.push(info.includeAlias);
|
|
7415
|
-
}
|
|
7416
|
-
let actualColumnName = null;
|
|
7417
|
-
for (const baseName of candidates) {
|
|
7418
|
-
const snakeJson = `${baseName}_json`;
|
|
7419
|
-
const camelJson = snakeJson.replace(
|
|
7420
|
-
/_([a-z])/g,
|
|
7421
|
-
(_, c) => c.toUpperCase()
|
|
7422
|
-
);
|
|
7423
|
-
if (snakeJson in record2) {
|
|
7424
|
-
actualColumnName = snakeJson;
|
|
7425
|
-
break;
|
|
7426
|
-
}
|
|
7427
|
-
if (camelJson in record2) {
|
|
7428
|
-
actualColumnName = camelJson;
|
|
7429
|
-
break;
|
|
7430
|
-
}
|
|
7431
|
-
}
|
|
7432
|
-
if (actualColumnName) {
|
|
7433
|
-
const jsonValue = record2[actualColumnName];
|
|
7434
|
-
let parsed;
|
|
7435
|
-
if (typeof jsonValue === "string") {
|
|
7436
|
-
try {
|
|
7437
|
-
parsed = JSON.parse(jsonValue);
|
|
7438
|
-
} catch {
|
|
7439
|
-
parsed = info.isToOne ? null : [];
|
|
7440
|
-
}
|
|
7441
|
-
} else if (Array.isArray(jsonValue)) {
|
|
7442
|
-
parsed = jsonValue;
|
|
7443
|
-
} else if (jsonValue === null || jsonValue === void 0) {
|
|
7444
|
-
parsed = info.isToOne ? null : [];
|
|
7445
|
-
} else {
|
|
7446
|
-
parsed = jsonValue;
|
|
7447
|
-
}
|
|
7448
|
-
if (info.isToOne && Array.isArray(parsed)) {
|
|
7449
|
-
parsed = parsed.length > 0 ? parsed[0] : null;
|
|
7450
|
-
}
|
|
7451
|
-
const outputKey = info.includeAlias ?? relationName;
|
|
7452
|
-
record2[outputKey] = parsed;
|
|
7453
|
-
delete record2[actualColumnName];
|
|
7454
|
-
}
|
|
7455
|
-
}
|
|
7456
|
-
}
|
|
7457
|
-
}
|
|
7942
|
+
// src/dx/result-hydrator.ts
|
|
7943
|
+
import { toColumnList as toColumnList3 } from "@dbsp/types";
|
|
7458
7944
|
|
|
7459
7945
|
// src/dx/relation-paths.ts
|
|
7460
7946
|
function nonEmptyString(value) {
|
|
@@ -7619,7 +8105,7 @@ var ResultHydrator = class {
|
|
|
7619
8105
|
for (const includeInfo of subqueryIncludes) {
|
|
7620
8106
|
const parentIds = [];
|
|
7621
8107
|
for (const r of results) {
|
|
7622
|
-
const id = this.
|
|
8108
|
+
const id = this.extractQueryKeyValue(
|
|
7623
8109
|
r,
|
|
7624
8110
|
includeInfo.sourceKey
|
|
7625
8111
|
);
|
|
@@ -7831,14 +8317,16 @@ var ResultHydrator = class {
|
|
|
7831
8317
|
* Get the foreign key column name from relation metadata.
|
|
7832
8318
|
*/
|
|
7833
8319
|
getForeignKeyColumn(foreignKey) {
|
|
7834
|
-
|
|
8320
|
+
const columns = toColumnList3(foreignKey);
|
|
8321
|
+
if (columns.length === 0) {
|
|
7835
8322
|
return "parent_id";
|
|
7836
8323
|
}
|
|
7837
|
-
if (
|
|
7838
|
-
|
|
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
|
+
);
|
|
7839
8328
|
}
|
|
7840
|
-
|
|
7841
|
-
return first ?? "parent_id";
|
|
8329
|
+
return columns[0];
|
|
7842
8330
|
}
|
|
7843
8331
|
/**
|
|
7844
8332
|
* Build traversal config for recursive CTE.
|
|
@@ -7930,10 +8418,14 @@ var ResultHydrator = class {
|
|
|
7930
8418
|
* case (slower but collision-safe for any value content).
|
|
7931
8419
|
*/
|
|
7932
8420
|
extractKeyValue(obj, key) {
|
|
7933
|
-
|
|
7934
|
-
|
|
8421
|
+
const columns = toColumnList3(key);
|
|
8422
|
+
if (columns.length === 0) {
|
|
8423
|
+
return void 0;
|
|
7935
8424
|
}
|
|
7936
|
-
|
|
8425
|
+
if (columns.length === 1) {
|
|
8426
|
+
return obj[columns[0]];
|
|
8427
|
+
}
|
|
8428
|
+
const values = columns.map((k) => obj[k]);
|
|
7937
8429
|
if (values.some((v2) => v2 === void 0 || v2 === null)) {
|
|
7938
8430
|
return void 0;
|
|
7939
8431
|
}
|
|
@@ -7943,6 +8435,17 @@ var ResultHydrator = class {
|
|
|
7943
8435
|
}
|
|
7944
8436
|
return parts.join(COMPOSITE_KEY_SEP);
|
|
7945
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
|
+
}
|
|
7946
8449
|
};
|
|
7947
8450
|
|
|
7948
8451
|
// src/dx/set-operation-builder.ts
|
|
@@ -10416,13 +10919,18 @@ function mapRelationType2(kind) {
|
|
|
10416
10919
|
return "belongsToMany";
|
|
10417
10920
|
}
|
|
10418
10921
|
}
|
|
10922
|
+
function isGeneratedTableWithConfig(table) {
|
|
10923
|
+
const columns = table.columns;
|
|
10924
|
+
return typeof columns === "object" && columns !== null && typeof columns.type !== "string";
|
|
10925
|
+
}
|
|
10419
10926
|
function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
|
|
10420
10927
|
const columns = [];
|
|
10421
10928
|
const foreignKeys = [];
|
|
10422
10929
|
const indexes = [];
|
|
10423
10930
|
const primaryKeys = [];
|
|
10931
|
+
const tableColumns = isGeneratedTableWithConfig(genTable) ? genTable.columns : genTable;
|
|
10424
10932
|
const indexedColumns = /* @__PURE__ */ new Set();
|
|
10425
|
-
for (const [colName, colDef] of Object.entries(
|
|
10933
|
+
for (const [colName, colDef] of Object.entries(tableColumns)) {
|
|
10426
10934
|
const col2 = {
|
|
10427
10935
|
name: colName,
|
|
10428
10936
|
type: generatedTypeToColumnType(colDef.type),
|
|
@@ -10475,7 +10983,7 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
|
|
|
10475
10983
|
const fkColumn = fk.columns[0];
|
|
10476
10984
|
const targetColumn = fk.references.columns[0];
|
|
10477
10985
|
if (fk.references.table === tableName && fkColumn !== void 0 && targetColumn !== void 0) {
|
|
10478
|
-
const colDef =
|
|
10986
|
+
const colDef = tableColumns[fkColumn];
|
|
10479
10987
|
const inferredName = fkColumn.endsWith("Id") ? fkColumn.slice(0, -2) : "parent";
|
|
10480
10988
|
const parentRole = colDef?.references?.parentRole ?? inferredName;
|
|
10481
10989
|
const childRole = colDef?.references?.childRole ?? (parentRole === "parent" ? "children" : `${parentRole}s`);
|
|
@@ -10491,7 +10999,10 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
|
|
|
10491
10999
|
}
|
|
10492
11000
|
}
|
|
10493
11001
|
let primaryKey;
|
|
10494
|
-
|
|
11002
|
+
const configuredPrimaryKey = isGeneratedTableWithConfig(genTable) ? genTable.primaryKey : void 0;
|
|
11003
|
+
if (configuredPrimaryKey !== void 0) {
|
|
11004
|
+
primaryKey = configuredPrimaryKey;
|
|
11005
|
+
} else if (primaryKeys.length > 0) {
|
|
10495
11006
|
primaryKey = primaryKeys.length === 1 ? primaryKeys[0] : primaryKeys;
|
|
10496
11007
|
} else {
|
|
10497
11008
|
const fkColumns = foreignKeys.flatMap((fk) => fk.columns);
|
|
@@ -10502,6 +11013,27 @@ function buildTableIRFromDefinition(tableName, genTable, fkAutoIndex) {
|
|
|
10502
11013
|
primaryKey = hasId ? "id" : void 0;
|
|
10503
11014
|
}
|
|
10504
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
|
+
}
|
|
10505
11037
|
const frozenColumns = Object.freeze(columns);
|
|
10506
11038
|
return Object.freeze({
|
|
10507
11039
|
name: tableName,
|
|
@@ -10685,6 +11217,7 @@ var ForeignKeyReferenceSchema = v.object({
|
|
|
10685
11217
|
parentRole: v.optional(v.string()),
|
|
10686
11218
|
childRole: v.optional(v.string())
|
|
10687
11219
|
});
|
|
11220
|
+
var StringOrStringArraySchema = v.union([v.string(), v.array(v.string())]);
|
|
10688
11221
|
var ColumnDefinitionSchema = v.object({
|
|
10689
11222
|
type: SchemaColumnTypeSchema,
|
|
10690
11223
|
primaryKey: v.optional(v.boolean()),
|
|
@@ -10698,7 +11231,24 @@ var ColumnDefinitionSchema = v.object({
|
|
|
10698
11231
|
var FlatTableDefinitionSchema = safeRecord(ColumnDefinitionSchema);
|
|
10699
11232
|
var TableDefWithConfigSchema = v.object({
|
|
10700
11233
|
columns: FlatTableDefinitionSchema,
|
|
10701
|
-
primaryKey: v.
|
|
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
|
+
),
|
|
10702
11252
|
indexes: v.optional(
|
|
10703
11253
|
v.array(
|
|
10704
11254
|
v.object({
|
|
@@ -10720,15 +11270,15 @@ var IncludeStrategySchema = v.optional(
|
|
|
10720
11270
|
var BelongsToRelationSchema = v.object({
|
|
10721
11271
|
kind: v.literal("belongsTo"),
|
|
10722
11272
|
target: v.string(),
|
|
10723
|
-
foreignKey:
|
|
10724
|
-
targetKey: v.optional(
|
|
11273
|
+
foreignKey: StringOrStringArraySchema,
|
|
11274
|
+
targetKey: v.optional(StringOrStringArraySchema),
|
|
10725
11275
|
includeStrategy: IncludeStrategySchema
|
|
10726
11276
|
});
|
|
10727
11277
|
var HasManyRelationSchema = v.object({
|
|
10728
11278
|
kind: v.literal("hasMany"),
|
|
10729
11279
|
target: v.string(),
|
|
10730
|
-
foreignKey:
|
|
10731
|
-
sourceKey: v.optional(
|
|
11280
|
+
foreignKey: StringOrStringArraySchema,
|
|
11281
|
+
sourceKey: v.optional(StringOrStringArraySchema),
|
|
10732
11282
|
includeStrategy: IncludeStrategySchema
|
|
10733
11283
|
});
|
|
10734
11284
|
var ManyToManyRelationSchema = v.object({
|