@prisma-next/sql-orm-client 0.5.0-dev.6 → 0.5.0-dev.61
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/README.md +31 -1
- package/dist/index.d.mts +29 -21
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +203 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -18
- package/src/collection-dispatch.ts +18 -2
- package/src/collection-mutation-dispatch.ts +1 -1
- package/src/collection-runtime.ts +3 -2
- package/src/collection.ts +29 -1
- package/src/execute-query-plan.ts +4 -5
- package/src/filters.ts +1 -1
- package/src/include-strategy.ts +36 -14
- package/src/model-accessor.ts +69 -63
- package/src/mutation-executor.ts +10 -2
- package/src/query-plan-aggregate.ts +32 -13
- package/src/query-plan-meta.ts +6 -66
- package/src/query-plan-mutations.ts +31 -23
- package/src/query-plan-select.ts +16 -6
- package/src/types.ts +88 -103
- package/src/where-binding.ts +10 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AsyncIterableResult } from "@prisma-next/runtime
|
|
2
|
-
import { AggregateExpr, AndExpr, BinaryExpr, ColumnRef, DefaultValueExpr, DeleteAst, DerivedTableSource, EqColJoinOn, ExistsExpr, InsertAst, InsertOnConflict, JoinAst, JsonArrayAggExpr, JsonObjectExpr, ListExpression, LiteralExpr, NotExpr, NullCheckExpr,
|
|
1
|
+
import { AsyncIterableResult } from "@prisma-next/framework-components/runtime";
|
|
2
|
+
import { AggregateExpr, AndExpr, BinaryExpr, ColumnRef, DefaultValueExpr, DeleteAst, DerivedTableSource, EqColJoinOn, ExistsExpr, InsertAst, InsertOnConflict, JoinAst, JsonArrayAggExpr, JsonObjectExpr, ListExpression, LiteralExpr, NotExpr, NullCheckExpr, OrExpr, OrderByItem, ParamRef, ProjectionItem, SelectAst, SubqueryExpr, TableSource, UpdateAst, collectOrderedParamRefs, isWhereExpr } from "@prisma-next/sql-relational-core/ast";
|
|
3
3
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
4
4
|
|
|
5
5
|
//#region src/collection-contract.ts
|
|
@@ -398,83 +398,70 @@ function executeQueryPlan(scope, plan) {
|
|
|
398
398
|
|
|
399
399
|
//#endregion
|
|
400
400
|
//#region src/include-strategy.ts
|
|
401
|
+
/**
|
|
402
|
+
* Choose the SQL emission strategy for nested includes based on the
|
|
403
|
+
* contract's declared capabilities.
|
|
404
|
+
*
|
|
405
|
+
* - `'lateral'`: outer SELECT with one LATERAL JOIN per relation,
|
|
406
|
+
* aggregating to JSON. Requires both `lateral` and `jsonAgg`.
|
|
407
|
+
* Postgres has both.
|
|
408
|
+
* - `'correlated'`: outer SELECT with one correlated subquery per
|
|
409
|
+
* relation, aggregating to JSON. Requires `jsonAgg` only.
|
|
410
|
+
* SQLite has `jsonAgg` (via `json_group_array`) but no LATERAL.
|
|
411
|
+
* - `'multiQuery'`: fallback. One SELECT per relation, stitched
|
|
412
|
+
* together in JS via `WHERE pk IN (parent-pk-values)`. Always
|
|
413
|
+
* correct; just N+1 round-trips.
|
|
414
|
+
*
|
|
415
|
+
* The capability flags are looked up under the contract's
|
|
416
|
+
* `targetFamily` and `target` namespaces — the two layers the contract
|
|
417
|
+
* emitter actually populates. Cross-namespace ("`postgres.lateral`
|
|
418
|
+
* found while running SQLite") false positives are impossible because
|
|
419
|
+
* we only inspect the running target's namespaces.
|
|
420
|
+
*/
|
|
401
421
|
function selectIncludeStrategy(contract) {
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const hasJsonAgg = hasCapability(capabilities?.["jsonAgg"]);
|
|
422
|
+
const hasLateral = capabilityFlag(contract, "lateral");
|
|
423
|
+
const hasJsonAgg = capabilityFlag(contract, "jsonAgg");
|
|
405
424
|
if (hasLateral && hasJsonAgg) return "lateral";
|
|
406
425
|
if (hasJsonAgg) return "correlated";
|
|
407
426
|
return "multiQuery";
|
|
408
427
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Read a capability flag from the contract's target/family namespaces.
|
|
430
|
+
*
|
|
431
|
+
* The contract emitter populates `capabilities[targetFamily]` (universal
|
|
432
|
+
* SQL flags like `jsonAgg`, `returning`) and `capabilities[target]`
|
|
433
|
+
* (target-specific flags like `lateral` on Postgres). Either may
|
|
434
|
+
* declare a given flag; the family namespace declares the floor and the
|
|
435
|
+
* target namespace can extend on top.
|
|
436
|
+
*/
|
|
437
|
+
function capabilityFlag(contract, flag) {
|
|
438
|
+
return contract.capabilities[contract.targetFamily]?.[flag] === true || contract.capabilities[contract.target]?.[flag] === true;
|
|
414
439
|
}
|
|
415
440
|
|
|
416
441
|
//#endregion
|
|
417
442
|
//#region src/query-plan-meta.ts
|
|
418
|
-
function resolveProjectionCodecs(contract, ast) {
|
|
419
|
-
const codecs = {};
|
|
420
|
-
if (ast.kind === "select") {
|
|
421
|
-
for (const item of ast.projection) if (item.expr.kind === "column-ref") {
|
|
422
|
-
const col = contract.storage.tables[item.expr.table]?.columns[item.expr.column];
|
|
423
|
-
if (col?.codecId) codecs[item.alias] = col.codecId;
|
|
424
|
-
}
|
|
425
|
-
} else if (ast.returning) {
|
|
426
|
-
const tableName = ast.table.name;
|
|
427
|
-
const table = contract.storage.tables[tableName];
|
|
428
|
-
if (!table) return void 0;
|
|
429
|
-
for (const colRef of ast.returning) {
|
|
430
|
-
const col = table.columns[colRef.column];
|
|
431
|
-
if (col?.codecId) codecs[colRef.column] = col.codecId;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return Object.keys(codecs).length > 0 ? codecs : void 0;
|
|
435
|
-
}
|
|
436
443
|
function deriveParamsFromAst(ast) {
|
|
437
|
-
|
|
438
|
-
return {
|
|
439
|
-
params: collectedParams.map((p) => p.value),
|
|
440
|
-
paramDescriptors: collectedParams.map((p) => ({
|
|
441
|
-
...ifDefined("name", p.name),
|
|
442
|
-
...ifDefined("codecId", p.codecId),
|
|
443
|
-
source: "dsl"
|
|
444
|
-
}))
|
|
445
|
-
};
|
|
444
|
+
return { params: collectOrderedParamRefs(ast).map((p) => p.value) };
|
|
446
445
|
}
|
|
447
446
|
function resolveTableColumns(contract, tableName) {
|
|
448
447
|
const table = contract.storage.tables[tableName];
|
|
449
448
|
if (!table) throw new Error(`Unknown table "${tableName}" in SQL ORM query planner`);
|
|
450
449
|
return Object.keys(table.columns);
|
|
451
450
|
}
|
|
452
|
-
function buildOrmPlanMeta(contract
|
|
451
|
+
function buildOrmPlanMeta(contract) {
|
|
453
452
|
return {
|
|
454
453
|
target: contract.target,
|
|
455
454
|
targetFamily: contract.targetFamily,
|
|
456
455
|
storageHash: contract.storage.storageHash,
|
|
457
456
|
...contract.profileHash !== void 0 ? { profileHash: contract.profileHash } : {},
|
|
458
|
-
lane: "orm-client"
|
|
459
|
-
paramDescriptors: [...paramDescriptors]
|
|
457
|
+
lane: "orm-client"
|
|
460
458
|
};
|
|
461
459
|
}
|
|
462
|
-
function buildOrmQueryPlan(contract, ast, params
|
|
463
|
-
const projectionTypes = resolveProjectionCodecs(contract, ast);
|
|
464
|
-
const codecAnnotations = projectionTypes ? { codecs: Object.freeze({ ...projectionTypes }) } : void 0;
|
|
465
|
-
const limitAnnotation = ast.kind === "select" && ast.limit !== void 0 ? { limit: ast.limit } : void 0;
|
|
466
|
-
const annotations = codecAnnotations || limitAnnotation ? Object.freeze({
|
|
467
|
-
...codecAnnotations,
|
|
468
|
-
...limitAnnotation
|
|
469
|
-
}) : void 0;
|
|
460
|
+
function buildOrmQueryPlan(contract, ast, params) {
|
|
470
461
|
return Object.freeze({
|
|
471
462
|
ast,
|
|
472
463
|
params: [...params],
|
|
473
|
-
meta:
|
|
474
|
-
...buildOrmPlanMeta(contract, paramDescriptors),
|
|
475
|
-
...ifDefined("projectionTypes", projectionTypes),
|
|
476
|
-
...ifDefined("annotations", annotations)
|
|
477
|
-
}
|
|
464
|
+
meta: buildOrmPlanMeta(contract)
|
|
478
465
|
});
|
|
479
466
|
}
|
|
480
467
|
|
|
@@ -488,10 +475,21 @@ function combineWhereExprs(filters) {
|
|
|
488
475
|
|
|
489
476
|
//#endregion
|
|
490
477
|
//#region src/query-plan-aggregate.ts
|
|
491
|
-
function
|
|
492
|
-
if (selector.fn === "count") return
|
|
478
|
+
function toAggregateProjection(contract, tableName, selector) {
|
|
479
|
+
if (selector.fn === "count") return {
|
|
480
|
+
expr: AggregateExpr.count(),
|
|
481
|
+
codecId: void 0
|
|
482
|
+
};
|
|
493
483
|
if (!selector.column) throw new Error(`Aggregate selector "${selector.fn}" requires a field`);
|
|
494
|
-
|
|
484
|
+
const expr = new AggregateExpr(selector.fn, ColumnRef.of(tableName, selector.column));
|
|
485
|
+
if (selector.fn === "min" || selector.fn === "max") return {
|
|
486
|
+
expr,
|
|
487
|
+
codecId: contract.storage.tables[tableName]?.columns[selector.column]?.codecId
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
expr,
|
|
491
|
+
codecId: void 0
|
|
492
|
+
};
|
|
495
493
|
}
|
|
496
494
|
function validateGroupedComparable(value) {
|
|
497
495
|
switch (value.kind) {
|
|
@@ -551,30 +549,39 @@ function validateGroupedHavingExpr(expr) {
|
|
|
551
549
|
function compileAggregate(contract, tableName, filters, aggregateSpec) {
|
|
552
550
|
const entries = Object.entries(aggregateSpec);
|
|
553
551
|
if (entries.length === 0) throw new Error("aggregate() requires at least one aggregation selector");
|
|
554
|
-
const projection = entries.map(([alias, selector]) =>
|
|
552
|
+
const projection = entries.map(([alias, selector]) => {
|
|
553
|
+
const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
|
|
554
|
+
return ProjectionItem.of(alias, expr, codecId);
|
|
555
|
+
});
|
|
555
556
|
let ast = SelectAst.from(TableSource.named(tableName)).withProjection(projection);
|
|
556
557
|
const where = combineWhereExprs(filters);
|
|
557
558
|
if (where) ast = ast.withWhere(where);
|
|
558
|
-
const { params
|
|
559
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
559
|
+
const { params } = deriveParamsFromAst(ast);
|
|
560
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
560
561
|
}
|
|
561
562
|
function compileGroupedAggregate(contract, tableName, filters, groupByColumns, aggregateSpec, havingExpr) {
|
|
562
563
|
if (groupByColumns.length === 0) throw new Error("groupBy() requires at least one field");
|
|
563
564
|
const entries = Object.entries(aggregateSpec);
|
|
564
565
|
if (entries.length === 0) throw new Error("groupBy().aggregate() requires at least one aggregation selector");
|
|
565
|
-
const
|
|
566
|
+
const table = contract.storage.tables[tableName];
|
|
567
|
+
const projection = [...groupByColumns.map((column) => ProjectionItem.of(column, ColumnRef.of(tableName, column), table?.columns[column]?.codecId)), ...entries.map(([alias, selector]) => {
|
|
568
|
+
const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
|
|
569
|
+
return ProjectionItem.of(alias, expr, codecId);
|
|
570
|
+
})];
|
|
566
571
|
let ast = SelectAst.from(TableSource.named(tableName)).withProjection(projection).withGroupBy(groupByColumns.map((column) => ColumnRef.of(tableName, column)));
|
|
567
572
|
const where = combineWhereExprs(filters);
|
|
568
573
|
if (where) ast = ast.withWhere(where);
|
|
569
574
|
if (havingExpr) ast = ast.withHaving(validateGroupedHavingExpr(havingExpr));
|
|
570
|
-
const { params
|
|
571
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
575
|
+
const { params } = deriveParamsFromAst(ast);
|
|
576
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
572
577
|
}
|
|
573
578
|
|
|
574
579
|
//#endregion
|
|
575
580
|
//#region src/query-plan-mutations.ts
|
|
576
581
|
function buildReturningColumns(contract, tableName, returningColumns) {
|
|
577
|
-
|
|
582
|
+
const columns = returningColumns && returningColumns.length > 0 ? [...returningColumns] : resolveTableColumns(contract, tableName);
|
|
583
|
+
const table = contract.storage.tables[tableName];
|
|
584
|
+
return columns.map((column) => ProjectionItem.of(column, ColumnRef.of(tableName, column), table?.columns[column]?.codecId));
|
|
578
585
|
}
|
|
579
586
|
function toParamAssignments(contract, tableName, values) {
|
|
580
587
|
const assignments = {};
|
|
@@ -585,7 +592,11 @@ function toParamAssignments(contract, tableName, values) {
|
|
|
585
592
|
if (!codecId) throw new Error(`Unknown column "${column}" in table "${tableName}"`);
|
|
586
593
|
assignments[column] = ParamRef.of(value, {
|
|
587
594
|
name: column,
|
|
588
|
-
codecId
|
|
595
|
+
codecId,
|
|
596
|
+
refs: {
|
|
597
|
+
table: tableName,
|
|
598
|
+
column
|
|
599
|
+
}
|
|
589
600
|
});
|
|
590
601
|
}
|
|
591
602
|
return { assignments };
|
|
@@ -610,7 +621,11 @@ function normalizeInsertRows(contract, tableName, rows) {
|
|
|
610
621
|
if (!codecId) throw new Error(`Unknown column "${column}" in table "${tableName}"`);
|
|
611
622
|
normalizedRow[column] = ParamRef.of(row[column], {
|
|
612
623
|
name: column,
|
|
613
|
-
codecId
|
|
624
|
+
codecId,
|
|
625
|
+
refs: {
|
|
626
|
+
table: tableName,
|
|
627
|
+
column
|
|
628
|
+
}
|
|
614
629
|
});
|
|
615
630
|
continue;
|
|
616
631
|
}
|
|
@@ -622,14 +637,14 @@ function normalizeInsertRows(contract, tableName, rows) {
|
|
|
622
637
|
function compileInsertReturning(contract, tableName, rows, returningColumns) {
|
|
623
638
|
const { rows: normalizedRows } = normalizeInsertRows(contract, tableName, rows);
|
|
624
639
|
const ast = InsertAst.into(TableSource.named(tableName)).withRows(normalizedRows).withReturning(buildReturningColumns(contract, tableName, returningColumns));
|
|
625
|
-
const { params
|
|
626
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
640
|
+
const { params } = deriveParamsFromAst(ast);
|
|
641
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
627
642
|
}
|
|
628
643
|
function compileInsertCount(contract, tableName, rows) {
|
|
629
644
|
const { rows: normalizedRows } = normalizeInsertRows(contract, tableName, rows);
|
|
630
645
|
const ast = InsertAst.into(TableSource.named(tableName)).withRows(normalizedRows);
|
|
631
|
-
const { params
|
|
632
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
646
|
+
const { params } = deriveParamsFromAst(ast);
|
|
647
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
633
648
|
}
|
|
634
649
|
function stripUndefinedValues(row) {
|
|
635
650
|
const result = {};
|
|
@@ -665,38 +680,38 @@ function compileUpsertReturning(contract, tableName, createValues, updateValues,
|
|
|
665
680
|
const updateAssignments = Object.keys(updateValues).length > 0 ? toParamAssignments(contract, tableName, updateValues) : void 0;
|
|
666
681
|
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();
|
|
667
682
|
const ast = InsertAst.into(TableSource.named(tableName)).withValues(createAssignments.assignments).withOnConflict(onConflict).withReturning(buildReturningColumns(contract, tableName, returningColumns));
|
|
668
|
-
const { params
|
|
669
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
683
|
+
const { params } = deriveParamsFromAst(ast);
|
|
684
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
670
685
|
}
|
|
671
686
|
function compileUpdateReturning(contract, tableName, setValues, filters, returningColumns) {
|
|
672
687
|
const where = combineWhereExprs(filters);
|
|
673
688
|
const { assignments } = toParamAssignments(contract, tableName, setValues);
|
|
674
689
|
let ast = UpdateAst.table(TableSource.named(tableName)).withSet(assignments).withReturning(buildReturningColumns(contract, tableName, returningColumns));
|
|
675
690
|
if (where) ast = ast.withWhere(where);
|
|
676
|
-
const { params
|
|
677
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
691
|
+
const { params } = deriveParamsFromAst(ast);
|
|
692
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
678
693
|
}
|
|
679
694
|
function compileUpdateCount(contract, tableName, setValues, filters) {
|
|
680
695
|
const where = combineWhereExprs(filters);
|
|
681
696
|
const { assignments } = toParamAssignments(contract, tableName, setValues);
|
|
682
697
|
let ast = UpdateAst.table(TableSource.named(tableName)).withSet(assignments);
|
|
683
698
|
if (where) ast = ast.withWhere(where);
|
|
684
|
-
const { params
|
|
685
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
699
|
+
const { params } = deriveParamsFromAst(ast);
|
|
700
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
686
701
|
}
|
|
687
702
|
function compileDeleteReturning(contract, tableName, filters, returningColumns) {
|
|
688
703
|
const where = combineWhereExprs(filters);
|
|
689
704
|
let ast = DeleteAst.from(TableSource.named(tableName)).withReturning(buildReturningColumns(contract, tableName, returningColumns));
|
|
690
705
|
if (where) ast = ast.withWhere(where);
|
|
691
|
-
const { params
|
|
692
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
706
|
+
const { params } = deriveParamsFromAst(ast);
|
|
707
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
693
708
|
}
|
|
694
709
|
function compileDeleteCount(contract, tableName, filters) {
|
|
695
710
|
const where = combineWhereExprs(filters);
|
|
696
711
|
let ast = DeleteAst.from(TableSource.named(tableName));
|
|
697
712
|
if (where) ast = ast.withWhere(where);
|
|
698
|
-
const { params
|
|
699
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
713
|
+
const { params } = deriveParamsFromAst(ast);
|
|
714
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
700
715
|
}
|
|
701
716
|
|
|
702
717
|
//#endregion
|
|
@@ -767,7 +782,13 @@ function bindComparable(contract, comparable, bindingColumn) {
|
|
|
767
782
|
function createParamRef(contract, columnRef, value) {
|
|
768
783
|
const codecId = contract.storage.tables[columnRef.table]?.columns[columnRef.column]?.codecId;
|
|
769
784
|
if (!codecId) throw new Error(`Unknown column "${columnRef.column}" in table "${columnRef.table}"`);
|
|
770
|
-
return ParamRef.of(value, {
|
|
785
|
+
return ParamRef.of(value, {
|
|
786
|
+
codecId,
|
|
787
|
+
refs: {
|
|
788
|
+
table: columnRef.table,
|
|
789
|
+
column: columnRef.column
|
|
790
|
+
}
|
|
791
|
+
});
|
|
771
792
|
}
|
|
772
793
|
function createExpressionBinder(contract) {
|
|
773
794
|
return { select: (ast) => bindSelectAst(contract, ast) };
|
|
@@ -796,7 +817,7 @@ function bindSelectAst(contract, ast) {
|
|
|
796
817
|
return new SelectAst({
|
|
797
818
|
from: bindFromSource(contract, ast.from),
|
|
798
819
|
joins: ast.joins?.map((join) => bindJoin(contract, join)),
|
|
799
|
-
projection: ast.projection.map((projection) => new ProjectionItem(projection.alias, bindProjectionExpr(contract, projection.expr))),
|
|
820
|
+
projection: ast.projection.map((projection) => new ProjectionItem(projection.alias, bindProjectionExpr(contract, projection.expr), projection.codecId, projection.refs)),
|
|
800
821
|
where: ast.where ? bindWhereExprNode(contract, ast.where) : void 0,
|
|
801
822
|
orderBy: ast.orderBy?.map((orderItem) => bindOrderByItem(contract, orderItem)),
|
|
802
823
|
distinct: ast.distinct,
|
|
@@ -812,7 +833,9 @@ function bindSelectAst(contract, ast) {
|
|
|
812
833
|
//#endregion
|
|
813
834
|
//#region src/query-plan-select.ts
|
|
814
835
|
function buildProjection(contract, tableName, selectedFields, tableRef = tableName) {
|
|
815
|
-
|
|
836
|
+
const columns = selectedFields && selectedFields.length > 0 ? [...selectedFields] : resolveTableColumns(contract, tableName);
|
|
837
|
+
const table = contract.storage.tables[tableName];
|
|
838
|
+
return columns.map((column) => ProjectionItem.of(column, ColumnRef.of(tableRef, column), table?.columns[column]?.codecId));
|
|
816
839
|
}
|
|
817
840
|
function createBoundaryExpr(tableName, entry) {
|
|
818
841
|
return new BinaryExpr(entry.direction === "asc" ? "gt" : "lt", ColumnRef.of(tableName, entry.column), LiteralExpr.of(entry.value));
|
|
@@ -946,10 +969,11 @@ function buildMtiJoins(contract, polyInfo, variantName) {
|
|
|
946
969
|
const join = joinType === "inner" ? JoinAst.inner(TableSource.named(variant.table), joinOn) : JoinAst.left(TableSource.named(variant.table), joinOn);
|
|
947
970
|
joins.push(join);
|
|
948
971
|
const variantColumns = resolveTableColumns(contract, variant.table);
|
|
972
|
+
const variantTable = contract.storage.tables[variant.table];
|
|
949
973
|
for (const col of variantColumns) {
|
|
950
974
|
if (col === pkColumn) continue;
|
|
951
975
|
const alias = `${variant.table}__${col}`;
|
|
952
|
-
projection.push(ProjectionItem.of(alias, ColumnRef.of(variant.table, col)));
|
|
976
|
+
projection.push(ProjectionItem.of(alias, ColumnRef.of(variant.table, col), variantTable?.columns[col]?.codecId));
|
|
953
977
|
}
|
|
954
978
|
}
|
|
955
979
|
return {
|
|
@@ -967,8 +991,8 @@ function compileSelect(contract, tableName, state, modelName) {
|
|
|
967
991
|
joins: mtiArtifacts.joins,
|
|
968
992
|
includeProjection: mtiArtifacts.projection
|
|
969
993
|
} : void 0);
|
|
970
|
-
const { params
|
|
971
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
994
|
+
const { params } = deriveParamsFromAst(ast);
|
|
995
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
972
996
|
}
|
|
973
997
|
function compileRelationSelect(contract, relatedTableName, targetColumn, parentPks, nestedState) {
|
|
974
998
|
const inFilter = BinaryExpr.in(ColumnRef.of(relatedTableName, targetColumn), ListExpression.fromValues(parentPks));
|
|
@@ -1009,8 +1033,8 @@ function compileSelectWithIncludeStrategy(contract, tableName, state, strategy,
|
|
|
1009
1033
|
includeProjection,
|
|
1010
1034
|
...topLevelWhere ? { where: topLevelWhere } : {}
|
|
1011
1035
|
});
|
|
1012
|
-
const { params
|
|
1013
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
1036
|
+
const { params } = deriveParamsFromAst(ast);
|
|
1037
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
1014
1038
|
}
|
|
1015
1039
|
|
|
1016
1040
|
//#endregion
|
|
@@ -1401,7 +1425,7 @@ function shorthandToWhereExpr(context, modelName, filters) {
|
|
|
1401
1425
|
function assertFieldHasEqualityTrait(context, modelName, fieldName) {
|
|
1402
1426
|
const fieldType = modelOf(context.contract, modelName)?.fields?.[fieldName]?.type;
|
|
1403
1427
|
const codecId = fieldType?.kind === "scalar" ? fieldType.codecId : void 0;
|
|
1404
|
-
if (!(codecId ? context.
|
|
1428
|
+
if (!(codecId ? context.codecDescriptors.descriptorFor(codecId)?.traits ?? [] : []).includes("equality")) throw new Error(`Shorthand filter on "${modelName}.${fieldName}": field does not support equality comparisons`);
|
|
1405
1429
|
}
|
|
1406
1430
|
|
|
1407
1431
|
//#endregion
|
|
@@ -1565,17 +1589,40 @@ function emptyState() {
|
|
|
1565
1589
|
variantName: void 0
|
|
1566
1590
|
};
|
|
1567
1591
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1592
|
+
/**
|
|
1593
|
+
* Resolve the unique column ref carried by `left` so its surrounding `ParamRef` can dispatch through `forColumn`. The previous implementation only matched bare `column-ref` expressions, which lost refs the moment the expression got wrapped (`upper(f.id)`, `BinaryExpr`, function-call expressions, etc.) — and column-aware dispatch (AC-5) silently degraded for any predicate that touched a computed column.
|
|
1594
|
+
*
|
|
1595
|
+
* Walking via `collectColumnRefs()` and accepting only a single unambiguous ref gives `eq(upper(f.id), value)` and friends correct dispatch without inventing refs for ambiguous shapes (e.g. `eq(concat(f.firstName, f.lastName), value)` returns `undefined` and falls through to the codec-id path, which is correct for non-parameterized comparisons).
|
|
1596
|
+
*/
|
|
1597
|
+
function refsFromLeft(left) {
|
|
1598
|
+
if (left.kind === "column-ref") return {
|
|
1599
|
+
table: left.table,
|
|
1600
|
+
column: left.column
|
|
1601
|
+
};
|
|
1602
|
+
const columnRefs = left.collectColumnRefs();
|
|
1603
|
+
if (columnRefs.length !== 1) return void 0;
|
|
1604
|
+
const single = columnRefs[0];
|
|
1605
|
+
if (!single) return void 0;
|
|
1606
|
+
return {
|
|
1607
|
+
table: single.table,
|
|
1608
|
+
column: single.column
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function param(codecId, value, refs) {
|
|
1612
|
+
if (codecId === void 0 && refs === void 0) return ParamRef.of(value);
|
|
1613
|
+
return ParamRef.of(value, {
|
|
1614
|
+
...ifDefined("codecId", codecId),
|
|
1615
|
+
...ifDefined("refs", refs)
|
|
1616
|
+
});
|
|
1570
1617
|
}
|
|
1571
|
-
function paramList(codecId, values) {
|
|
1572
|
-
return ListExpression.of(values.map((value) => param(codecId, value)));
|
|
1618
|
+
function paramList(codecId, values, refs) {
|
|
1619
|
+
return ListExpression.of(values.map((value) => param(codecId, value, refs)));
|
|
1573
1620
|
}
|
|
1574
1621
|
function scalarComparisonMethod(op) {
|
|
1575
|
-
return ((left, codecId) => (value) => new BinaryExpr(op, left, param(codecId, value)));
|
|
1622
|
+
return ((left, codecId) => (value) => new BinaryExpr(op, left, param(codecId, value, refsFromLeft(left))));
|
|
1576
1623
|
}
|
|
1577
1624
|
function listComparisonMethod(op) {
|
|
1578
|
-
return ((left, codecId) => (values) => new BinaryExpr(op, left, paramList(codecId, values)));
|
|
1625
|
+
return ((left, codecId) => (values) => new BinaryExpr(op, left, paramList(codecId, values, refsFromLeft(left))));
|
|
1579
1626
|
}
|
|
1580
1627
|
/**
|
|
1581
1628
|
* Declares trait requirements and runtime factory for each comparison method.
|
|
@@ -1655,12 +1702,13 @@ function createModelAccessor(context, modelName) {
|
|
|
1655
1702
|
existing.push(op);
|
|
1656
1703
|
}
|
|
1657
1704
|
for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
|
|
1658
|
-
const self = entry.args[0];
|
|
1659
1705
|
const op = [name, entry];
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1706
|
+
const self = entry.self;
|
|
1707
|
+
if (!self) continue;
|
|
1708
|
+
if (self.codecId !== void 0) registerOp(self.codecId, op);
|
|
1709
|
+
else if (self.traits !== void 0) for (const descriptor of context.codecDescriptors.values()) {
|
|
1710
|
+
const descriptorTraits = descriptor.traits;
|
|
1711
|
+
if (self.traits.every((t) => descriptorTraits.includes(t))) registerOp(descriptor.codecId, op);
|
|
1664
1712
|
}
|
|
1665
1713
|
}
|
|
1666
1714
|
return new Proxy({}, { get(_target, prop) {
|
|
@@ -1668,48 +1716,51 @@ function createModelAccessor(context, modelName) {
|
|
|
1668
1716
|
const relation = modelRelations[prop];
|
|
1669
1717
|
if (relation) return createRelationFilterAccessor(context, modelName, tableName, relation);
|
|
1670
1718
|
const columnName = fieldToColumn[prop] ?? prop;
|
|
1671
|
-
const
|
|
1672
|
-
|
|
1673
|
-
|
|
1719
|
+
const column = resolveColumn(contract, tableName, columnName);
|
|
1720
|
+
if (!column) return;
|
|
1721
|
+
const traits = context.codecDescriptors.descriptorFor(column.codecId)?.traits ?? [];
|
|
1722
|
+
const operations = opsByCodecId.get(column.codecId) ?? [];
|
|
1723
|
+
return createScalarFieldAccessor(tableName, columnName, column.codecId, column.nullable, traits, operations, context);
|
|
1674
1724
|
} });
|
|
1675
1725
|
}
|
|
1676
|
-
function
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
return (contract.storage.tables?.[tableName])?.columns?.[columnName]?.codecId;
|
|
1726
|
+
function resolveColumn(contract, tableName, columnName) {
|
|
1727
|
+
const column = contract.storage.tables?.[tableName]?.columns?.[columnName];
|
|
1728
|
+
if (!column) return void 0;
|
|
1729
|
+
return {
|
|
1730
|
+
codecId: column.codecId,
|
|
1731
|
+
nullable: column.nullable
|
|
1732
|
+
};
|
|
1684
1733
|
}
|
|
1685
|
-
function createScalarFieldAccessor(tableName, columnName, codecId, traits, operations, context) {
|
|
1734
|
+
function createScalarFieldAccessor(tableName, columnName, codecId, nullable, traits, operations, context) {
|
|
1686
1735
|
const column = ColumnRef.of(tableName, columnName);
|
|
1687
|
-
const
|
|
1736
|
+
const comparisonEntries = [];
|
|
1688
1737
|
for (const [name, meta] of Object.entries(COMPARISON_METHODS_META)) {
|
|
1689
1738
|
if (meta.traits.some((t) => !traits.includes(t))) continue;
|
|
1690
|
-
|
|
1739
|
+
comparisonEntries.push([name, meta.create(column, codecId)]);
|
|
1691
1740
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1741
|
+
const accessor = {
|
|
1742
|
+
returnType: {
|
|
1743
|
+
codecId,
|
|
1744
|
+
nullable
|
|
1745
|
+
},
|
|
1746
|
+
buildAst: () => column,
|
|
1747
|
+
...Object.fromEntries(comparisonEntries)
|
|
1748
|
+
};
|
|
1749
|
+
for (const [name, entry] of operations) accessor[name] = createExtensionMethodFactory(accessor, entry, context);
|
|
1750
|
+
return accessor;
|
|
1694
1751
|
}
|
|
1695
|
-
function createExtensionMethodFactory(
|
|
1696
|
-
const returnTraits = context.codecs.traitsOf(entry.returns.codecId);
|
|
1697
|
-
const isPredicate = returnTraits.includes("boolean");
|
|
1752
|
+
function createExtensionMethodFactory(selfExpr, entry, context) {
|
|
1698
1753
|
return (...args) => {
|
|
1699
|
-
const
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
returns: entry.returns,
|
|
1706
|
-
lowering: entry.lowering
|
|
1707
|
-
});
|
|
1708
|
-
if (isPredicate) return opExpr;
|
|
1754
|
+
const impl = entry.impl;
|
|
1755
|
+
const result = impl(selfExpr, ...args);
|
|
1756
|
+
const returnCodecId = result.returnType.codecId;
|
|
1757
|
+
const returnTraits = context.codecDescriptors.descriptorFor(returnCodecId)?.traits ?? [];
|
|
1758
|
+
if (returnTraits.includes("boolean")) return result.buildAst();
|
|
1759
|
+
const resultAst = result.buildAst();
|
|
1709
1760
|
const methods = {};
|
|
1710
1761
|
for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
|
|
1711
1762
|
if (meta.traits.some((t) => !returnTraits.includes(t))) continue;
|
|
1712
|
-
methods[resultMethodName] = meta.create(
|
|
1763
|
+
methods[resultMethodName] = meta.create(resultAst, returnCodecId);
|
|
1713
1764
|
}
|
|
1714
1765
|
return methods;
|
|
1715
1766
|
};
|
|
@@ -1756,7 +1807,7 @@ function toRelationWhereExpr(context, relatedModelName, predicate) {
|
|
|
1756
1807
|
for (const [fieldName, value] of Object.entries(predicate)) {
|
|
1757
1808
|
if (value === void 0) continue;
|
|
1758
1809
|
const fieldAccessor = accessor[fieldName];
|
|
1759
|
-
if (!fieldAccessor)
|
|
1810
|
+
if (!fieldAccessor) throw new Error(`Shorthand filter on "${relatedModelName}.${fieldName}": field is not defined on the model`);
|
|
1760
1811
|
if (value === null) {
|
|
1761
1812
|
if (!fieldAccessor.isNull) throw new Error(`Shorthand filter on "${relatedModelName}.${fieldName}": isNull is unexpectedly missing — this is a bug in trait gating`);
|
|
1762
1813
|
exprs.push(fieldAccessor.isNull());
|
|
@@ -1895,9 +1946,16 @@ async function updateFirstGraph(scope, context, modelName, filters, input) {
|
|
|
1895
1946
|
let parentRow = existingRow;
|
|
1896
1947
|
const mappedUpdateData = mapModelDataToStorageRow(contract, modelName, scalarData);
|
|
1897
1948
|
if (Object.keys(mappedUpdateData).length > 0) {
|
|
1949
|
+
const tableName = resolveModelTableName(contract, modelName);
|
|
1950
|
+
const appliedUpdateDefaults = context.applyMutationDefaults({
|
|
1951
|
+
op: "update",
|
|
1952
|
+
table: tableName,
|
|
1953
|
+
values: mappedUpdateData
|
|
1954
|
+
});
|
|
1955
|
+
for (const def of appliedUpdateDefaults) mappedUpdateData[def.column] = def.value;
|
|
1898
1956
|
const pkWhere = shorthandToWhereExpr(context, modelName, buildPrimaryKeyFilterFromRow(contract, modelName, existingRow));
|
|
1899
1957
|
if (!pkWhere) throw new Error(`Failed to build primary key filter for model "${modelName}"`);
|
|
1900
|
-
const updatedRaw = (await executeQueryPlan(scope, compileUpdateReturning(contract,
|
|
1958
|
+
const updatedRaw = (await executeQueryPlan(scope, compileUpdateReturning(contract, tableName, mappedUpdateData, [pkWhere], void 0)).toArray())[0];
|
|
1901
1959
|
if (updatedRaw) parentRow = mapStorageRowToModelFields(contract, modelName, updatedRaw);
|
|
1902
1960
|
}
|
|
1903
1961
|
for (const relationMutation of childOwned) await applyChildOwnedMutation(scope, context, modelName, parentRow, relationMutation.relation, relationMutation.mutation);
|
|
@@ -2109,15 +2167,25 @@ function isToWhereExpr(arg) {
|
|
|
2109
2167
|
//#endregion
|
|
2110
2168
|
//#region src/collection.ts
|
|
2111
2169
|
function applyCreateDefaults(ctx, tableName, rows) {
|
|
2170
|
+
const defaultValueCache = rows.length > 1 ? /* @__PURE__ */ new Map() : void 0;
|
|
2112
2171
|
for (const row of rows) {
|
|
2113
2172
|
const applied = ctx.context.applyMutationDefaults({
|
|
2114
2173
|
op: "create",
|
|
2115
2174
|
table: tableName,
|
|
2116
|
-
values: row
|
|
2175
|
+
values: row,
|
|
2176
|
+
...defaultValueCache ? { defaultValueCache } : {}
|
|
2117
2177
|
});
|
|
2118
2178
|
for (const def of applied) row[def.column] = def.value;
|
|
2119
2179
|
}
|
|
2120
2180
|
}
|
|
2181
|
+
function applyUpdateDefaults(ctx, tableName, values) {
|
|
2182
|
+
const applied = ctx.context.applyMutationDefaults({
|
|
2183
|
+
op: "update",
|
|
2184
|
+
table: tableName,
|
|
2185
|
+
values
|
|
2186
|
+
});
|
|
2187
|
+
for (const def of applied) values[def.column] = def.value;
|
|
2188
|
+
}
|
|
2121
2189
|
function isToWhereExprInput(value) {
|
|
2122
2190
|
return typeof value === "object" && value !== null && "toWhereExpr" in value && typeof value.toWhereExpr === "function";
|
|
2123
2191
|
}
|
|
@@ -2480,6 +2548,7 @@ var Collection = class Collection {
|
|
|
2480
2548
|
applyCreateDefaults(this.ctx, this.tableName, [createValues]);
|
|
2481
2549
|
const updateValues = mapModelDataToStorageRow(this.contract, this.modelName, input.update);
|
|
2482
2550
|
const hasUpdateValues = Object.keys(updateValues).length > 0;
|
|
2551
|
+
if (hasUpdateValues) applyUpdateDefaults(this.ctx, this.tableName, updateValues);
|
|
2483
2552
|
const conflictColumns = resolveUpsertConflictColumns(this.contract, this.modelName, input.conflictOn);
|
|
2484
2553
|
if (conflictColumns.length === 0) throw new Error(`upsert() for model "${this.modelName}" requires conflict columns`);
|
|
2485
2554
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
@@ -2526,6 +2595,7 @@ var Collection = class Collection {
|
|
|
2526
2595
|
const generator = async function* () {};
|
|
2527
2596
|
return new AsyncIterableResult(generator());
|
|
2528
2597
|
}
|
|
2598
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
2529
2599
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
2530
2600
|
const { selectedForQuery: selectedForUpdate, hiddenColumns } = augmentSelectionForJoinColumns(this.state.selectedFields, parentJoinColumns);
|
|
2531
2601
|
const compiled = compileUpdateReturning(this.contract, this.tableName, mappedData, this.state.filters, selectedForUpdate);
|
|
@@ -2542,6 +2612,7 @@ var Collection = class Collection {
|
|
|
2542
2612
|
async updateCount(data) {
|
|
2543
2613
|
const mappedData = mapModelDataToStorageRow(this.contract, this.modelName, data);
|
|
2544
2614
|
if (Object.keys(mappedData).length === 0) return 0;
|
|
2615
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
2545
2616
|
const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
|
|
2546
2617
|
const countState = {
|
|
2547
2618
|
...emptyState(),
|