@prisma-next/sql-orm-client 0.10.0-dev.2 → 0.10.0-dev.20
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.mjs +110 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +20 -20
- package/src/collection-dispatch.ts +77 -21
- package/src/include-tree-predicates.ts +50 -0
- package/src/query-plan-mutations.ts +1 -1
- package/src/query-plan-select.ts +99 -5
package/dist/index.mjs
CHANGED
|
@@ -442,6 +442,44 @@ function capabilityFlag(contract, flag) {
|
|
|
442
442
|
return contract.capabilities[contract.targetFamily]?.[flag] === true || contract.capabilities[contract.target]?.[flag] === true;
|
|
443
443
|
}
|
|
444
444
|
//#endregion
|
|
445
|
+
//#region src/include-tree-predicates.ts
|
|
446
|
+
/**
|
|
447
|
+
* Recursive predicate: does any include in the tree carry a
|
|
448
|
+
* non-leaf `distinct()` — i.e. `nested.distinct` set on an include
|
|
449
|
+
* whose `nested.includes` is non-empty?
|
|
450
|
+
*
|
|
451
|
+
* Such shapes cannot be lowered into the lateral / correlated
|
|
452
|
+
* single-query strategies: the child SELECT would emit
|
|
453
|
+
* `SELECT DISTINCT <scalars>, json_agg(<nested>) FROM ...`, and
|
|
454
|
+
* Postgres rejects equality on `json`. The dispatch path routes
|
|
455
|
+
* these to multi-query (which applies distinct to scalar-only rows
|
|
456
|
+
* before grandchildren stitch in JS); the planner rejects them at
|
|
457
|
+
* the boundary.
|
|
458
|
+
*
|
|
459
|
+
* `distinctOn` is intentionally not included: Postgres only
|
|
460
|
+
* compares the `ON (...)` expressions for equality, so a hashable
|
|
461
|
+
* key column plus json projections is well-defined.
|
|
462
|
+
*/
|
|
463
|
+
function hasNonLeafIncludeWithDistinct(includes) {
|
|
464
|
+
return includes.some((include) => include.nested.distinct !== void 0 && include.nested.distinct.length > 0 && include.nested.includes.length > 0 || hasNonLeafIncludeWithDistinct(include.nested.includes));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Recursive predicate: does any include in the tree carry a scalar
|
|
468
|
+
* selector (`count` / `sum` / ...) or a `combine()` descriptor?
|
|
469
|
+
*
|
|
470
|
+
* Such shapes cannot be lowered into the lateral / correlated
|
|
471
|
+
* single-query strategies (TML-2595). The dispatch path uses this
|
|
472
|
+
* to gate the whole tree to multi-query at any depth; the planner
|
|
473
|
+
* (`compileSelectWithIncludeStrategy`) uses the same predicate to
|
|
474
|
+
* fail fast at the boundary rather than build a malformed plan.
|
|
475
|
+
* Without the recursion, a depth-2+ row include containing a
|
|
476
|
+
* depth-3 `count()` would fall through to the planner and hit its
|
|
477
|
+
* explicit `throw` instead of routing to multi-query.
|
|
478
|
+
*/
|
|
479
|
+
function hasScalarOrCombineIncludeDescriptors(includes) {
|
|
480
|
+
return includes.some((include) => include.scalar !== void 0 || include.combine !== void 0 || hasScalarOrCombineIncludeDescriptors(include.nested.includes));
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
445
483
|
//#region src/query-plan-meta.ts
|
|
446
484
|
function deriveParamsFromAst(ast) {
|
|
447
485
|
return { params: collectOrderedParamRefs(ast).map((p) => p.kind === "param-ref" ? p.value : void 0) };
|
|
@@ -708,7 +746,7 @@ function compileUpsertReturning(contract, tableName, createValues, updateValues,
|
|
|
708
746
|
const createAssignments = toParamAssignments(contract, tableName, createValues);
|
|
709
747
|
const updateAssignments = Object.keys(updateValues).length > 0 ? toParamAssignments(contract, tableName, updateValues) : void 0;
|
|
710
748
|
const onConflict = updateAssignments ? InsertOnConflict.on(conflictColumns.map((column) => ColumnRef.of(tableName, column))).doUpdateSet(updateAssignments.assignments) : InsertOnConflict.on(conflictColumns.map((column) => ColumnRef.of(tableName, column))).doNothing();
|
|
711
|
-
const ast = InsertAst.into(TableSource.named(tableName)).
|
|
749
|
+
const ast = InsertAst.into(TableSource.named(tableName)).withRows([createAssignments.assignments]).withOnConflict(onConflict).withReturning(buildReturningColumns(contract, tableName, returningColumns));
|
|
712
750
|
const { params } = deriveParamsFromAst(ast);
|
|
713
751
|
return buildOrmQueryPlan(contract, ast, params);
|
|
714
752
|
}
|
|
@@ -927,17 +965,55 @@ function buildIncludeOrderArtifacts(relationName, rowAlias, childOrderBy) {
|
|
|
927
965
|
})
|
|
928
966
|
};
|
|
929
967
|
}
|
|
930
|
-
|
|
968
|
+
/**
|
|
969
|
+
* Recursively build the join + projection artifacts for the nested
|
|
970
|
+
* includes attached to a child SELECT. Used by
|
|
971
|
+
* `buildIncludeChildRowsSelect` to wire depth-2+ aggregates into the
|
|
972
|
+
* inner SELECT at each level.
|
|
973
|
+
*
|
|
974
|
+
* Under the `lateral` strategy each nested include contributes one
|
|
975
|
+
* LEFT JOIN LATERAL plus one projection item that references the
|
|
976
|
+
* lateral alias. Under the `correlated` strategy each nested include
|
|
977
|
+
* contributes a single projection item whose expression is a
|
|
978
|
+
* correlated subquery; no joins are produced. The two cases are
|
|
979
|
+
* symmetric, which is why both paths share `buildIncludeChildRowsSelect`.
|
|
980
|
+
*/
|
|
981
|
+
function buildNestedIncludeArtifacts(contract, parentTableRef, includes, strategy) {
|
|
982
|
+
if (includes.length === 0) return {
|
|
983
|
+
joins: [],
|
|
984
|
+
projections: []
|
|
985
|
+
};
|
|
986
|
+
const joins = [];
|
|
987
|
+
const projections = [];
|
|
988
|
+
for (const nested of includes) {
|
|
989
|
+
if (strategy === "lateral") {
|
|
990
|
+
const artifact = buildLateralIncludeArtifacts(contract, parentTableRef, nested);
|
|
991
|
+
joins.push(artifact.join);
|
|
992
|
+
projections.push(artifact.projection);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const artifact = buildCorrelatedIncludeProjection(contract, parentTableRef, nested);
|
|
996
|
+
projections.push(artifact.projection);
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
joins,
|
|
1000
|
+
projections
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function buildIncludeChildRowsSelect(contract, parentTableName, include, strategy) {
|
|
931
1004
|
const childState = include.nested;
|
|
932
1005
|
const childTableAlias = include.relatedTableName === parentTableName ? `${include.relationName}__child` : void 0;
|
|
933
1006
|
const childTableRef = childTableAlias ?? include.relatedTableName;
|
|
934
1007
|
const rowsAlias = `${include.relationName}__rows`;
|
|
935
|
-
const
|
|
936
|
-
const
|
|
1008
|
+
const scalarProjection = buildProjection(contract, include.relatedTableName, childState.selectedFields, childTableRef);
|
|
1009
|
+
const remappedChildOrderBy = childTableAlias && childState.orderBy ? childState.orderBy.map((item) => item.rewrite(createTableRefRemapper(include.relatedTableName, childTableRef))) : childState.orderBy;
|
|
1010
|
+
const { childOrderBy, hiddenOrderProjection, aggregateOrderBy } = buildIncludeOrderArtifacts(include.relationName, rowsAlias, remappedChildOrderBy);
|
|
937
1011
|
const childWhere = buildStateWhere(contract, childTableRef, childState, { filterTableName: include.relatedTableName });
|
|
938
1012
|
const joinExpr = BinaryExpr.eq(ColumnRef.of(childTableRef, include.targetColumn), ColumnRef.of(parentTableName, include.localColumn));
|
|
939
1013
|
const whereExpr = childWhere ? AndExpr.of([joinExpr, childWhere]) : joinExpr;
|
|
940
|
-
|
|
1014
|
+
const { joins: nestedJoins, projections: nestedProjections } = buildNestedIncludeArtifacts(contract, childTableRef, childState.includes, strategy);
|
|
1015
|
+
const childProjection = [...scalarProjection, ...nestedProjections];
|
|
1016
|
+
let childRows = SelectAst.from(TableSource.named(include.relatedTableName, childTableAlias)).withProjection([...childProjection, ...hiddenOrderProjection]).withJoins(nestedJoins).withWhere(whereExpr);
|
|
941
1017
|
if (childOrderBy) childRows = childRows.withOrderBy(childOrderBy);
|
|
942
1018
|
if (childState.distinctOn && childState.distinctOn.length > 0) childRows = childRows.withDistinctOn(childState.distinctOn.map((column) => ColumnRef.of(childTableRef, column)));
|
|
943
1019
|
else if (childState.distinct && childState.distinct.length > 0) childRows = childRows.withDistinct(true);
|
|
@@ -951,7 +1027,7 @@ function buildIncludeChildRowsSelect(contract, parentTableName, include) {
|
|
|
951
1027
|
};
|
|
952
1028
|
}
|
|
953
1029
|
function buildLateralIncludeArtifacts(contract, parentTableName, include) {
|
|
954
|
-
const { childRows, childProjection, rowsAlias, aggregateOrderBy } = buildIncludeChildRowsSelect(contract, parentTableName, include);
|
|
1030
|
+
const { childRows, childProjection, rowsAlias, aggregateOrderBy } = buildIncludeChildRowsSelect(contract, parentTableName, include, "lateral");
|
|
955
1031
|
const lateralAlias = `${include.relationName}_lateral`;
|
|
956
1032
|
const jsonObjectExpr = JsonObjectExpr.fromEntries(childProjection.map((item) => JsonObjectExpr.entry(item.alias, ColumnRef.of(rowsAlias, item.alias))));
|
|
957
1033
|
const aggregateQuery = SelectAst.from(DerivedTableSource.as(rowsAlias, childRows)).withProjection([ProjectionItem.of(include.relationName, JsonArrayAggExpr.of(jsonObjectExpr, "emptyArray", aggregateOrderBy))]);
|
|
@@ -961,7 +1037,7 @@ function buildLateralIncludeArtifacts(contract, parentTableName, include) {
|
|
|
961
1037
|
};
|
|
962
1038
|
}
|
|
963
1039
|
function buildCorrelatedIncludeProjection(contract, parentTableName, include) {
|
|
964
|
-
const { childRows, childProjection, rowsAlias, aggregateOrderBy } = buildIncludeChildRowsSelect(contract, parentTableName, include);
|
|
1040
|
+
const { childRows, childProjection, rowsAlias, aggregateOrderBy } = buildIncludeChildRowsSelect(contract, parentTableName, include, "correlated");
|
|
965
1041
|
const jsonObjectExpr = JsonObjectExpr.fromEntries(childProjection.map((item) => JsonObjectExpr.entry(item.alias, ColumnRef.of(rowsAlias, item.alias))));
|
|
966
1042
|
const aggregateQuery = SelectAst.from(DerivedTableSource.as(rowsAlias, childRows)).withProjection([ProjectionItem.of(include.relationName, JsonArrayAggExpr.of(jsonObjectExpr, "emptyArray", aggregateOrderBy))]);
|
|
967
1043
|
return { projection: ProjectionItem.of(include.relationName, SubqueryExpr.of(aggregateQuery)) };
|
|
@@ -1026,7 +1102,8 @@ function compileRelationSelect(contract, relatedTableName, targetColumn, parentP
|
|
|
1026
1102
|
});
|
|
1027
1103
|
}
|
|
1028
1104
|
function compileSelectWithIncludeStrategy(contract, tableName, state, strategy, modelName) {
|
|
1029
|
-
if (state.includes
|
|
1105
|
+
if (hasScalarOrCombineIncludeDescriptors(state.includes)) throw new Error("single-query include strategy does not support scalar include selectors or combine()");
|
|
1106
|
+
if (hasNonLeafIncludeWithDistinct(state.includes)) throw new Error("single-query include strategy does not support distinct() on a non-leaf include");
|
|
1030
1107
|
const includeJoins = [];
|
|
1031
1108
|
const includeProjection = [];
|
|
1032
1109
|
const topLevelWhere = buildStateWhere(contract, tableName, state);
|
|
@@ -1067,7 +1144,7 @@ function dispatchCollectionRows(options) {
|
|
|
1067
1144
|
}
|
|
1068
1145
|
function dispatchWithIncludeStrategy(options) {
|
|
1069
1146
|
const strategy = selectIncludeStrategy(options.contract);
|
|
1070
|
-
if (
|
|
1147
|
+
if (hasScalarOrCombineIncludeDescriptors(options.state.includes) || hasNonLeafIncludeWithDistinct(options.state.includes)) return dispatchWithMultiQueryIncludes(options);
|
|
1071
1148
|
switch (strategy) {
|
|
1072
1149
|
case "lateral": return dispatchWithSingleQueryIncludes({
|
|
1073
1150
|
...options,
|
|
@@ -1100,11 +1177,7 @@ function dispatchWithSingleQueryIncludes(options) {
|
|
|
1100
1177
|
};
|
|
1101
1178
|
});
|
|
1102
1179
|
for (const parent of parentRows) {
|
|
1103
|
-
for (const include of state.includes)
|
|
1104
|
-
if (include.scalar || include.combine) throw new Error("single-query include strategy does not support scalar include selectors or combine()");
|
|
1105
|
-
const mappedChildren = parseIncludedRows(parent.raw[include.relationName]).map((childRow) => mapStorageRowToModelFields(contract, include.relatedModelName, childRow));
|
|
1106
|
-
parent.mapped[include.relationName] = coerceSingleQueryIncludeResult(mappedChildren, include.cardinality);
|
|
1107
|
-
}
|
|
1180
|
+
for (const include of state.includes) parent.mapped[include.relationName] = decodeIncludePayload(contract, include, parent.raw[include.relationName]);
|
|
1108
1181
|
if (hiddenParentColumns.length > 0) stripHiddenMappedFields(contract, modelName, parent.mapped, hiddenParentColumns);
|
|
1109
1182
|
}
|
|
1110
1183
|
for (const row of parentRows) yield row.mapped;
|
|
@@ -1199,7 +1272,9 @@ async function stitchRowInclude(scope, contract, parentRows, include, state, par
|
|
|
1199
1272
|
}
|
|
1200
1273
|
}
|
|
1201
1274
|
async function resolveRowsByParent(scope, contract, include, state, parentJoinValues) {
|
|
1202
|
-
const
|
|
1275
|
+
const nestedJoinColumns = state.includes.map((nested) => nested.localColumn);
|
|
1276
|
+
const requiredChildColumns = Array.from(new Set([include.targetColumn, ...nestedJoinColumns]));
|
|
1277
|
+
const { selectedForQuery: childSelectedForQuery, hiddenColumns: hiddenChildColumns } = augmentSelectionForJoinColumns(state.selectedFields, requiredChildColumns);
|
|
1203
1278
|
const childRows = (await executeQueryPlan(scope, compileRelationSelect(contract, include.relatedTableName, include.targetColumn, parentJoinValues, {
|
|
1204
1279
|
...state,
|
|
1205
1280
|
selectedFields: childSelectedForQuery
|
|
@@ -1246,11 +1321,26 @@ async function resolveScalarByParent(scope, contract, include, selector, parentJ
|
|
|
1246
1321
|
function uniqueValues(values) {
|
|
1247
1322
|
return [...new Set(values)];
|
|
1248
1323
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Decode a single-query include payload from a parent row's raw cell
|
|
1326
|
+
* into the model-shaped value that downstream consumers see. Recurses
|
|
1327
|
+
* through `include.nested.includes` so depth-2+ trees — emitted by the
|
|
1328
|
+
* recursive lateral / correlated builders — are decoded symmetrically.
|
|
1329
|
+
*
|
|
1330
|
+
* The shape produced by the SQL side is one JSON column per top-level
|
|
1331
|
+
* include; values nested inside that JSON are already-parsed JS values
|
|
1332
|
+
* after the outer `JSON.parse`, so `parseIncludedRows` recognises both
|
|
1333
|
+
* the string (top-level) and array (nested) forms.
|
|
1334
|
+
*/
|
|
1335
|
+
function decodeIncludePayload(contract, include, raw) {
|
|
1336
|
+
return coerceSingleQueryIncludeResult(parseIncludedRows(raw).map((childRow) => {
|
|
1337
|
+
const mapped = mapStorageRowToModelFields(contract, include.relatedModelName, childRow);
|
|
1338
|
+
for (const nestedInclude of include.nested.includes) {
|
|
1339
|
+
if (nestedInclude.scalar || nestedInclude.combine) throw new Error("single-query include strategy does not support nested scalar include selectors or combine()");
|
|
1340
|
+
mapped[nestedInclude.relationName] = decodeIncludePayload(contract, nestedInclude, mapped[nestedInclude.relationName]);
|
|
1341
|
+
}
|
|
1342
|
+
return mapped;
|
|
1343
|
+
}), include.cardinality);
|
|
1254
1344
|
}
|
|
1255
1345
|
function assignEmptyIncludeResult(parentRows, include) {
|
|
1256
1346
|
if (include.combine) {
|