@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/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-orm-client",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.61",
|
|
4
|
+
"license": "Apache-2.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"sideEffects": false,
|
|
6
7
|
"description": "ORM client for Prisma Next — fluent, type-safe model collections",
|
|
7
8
|
"dependencies": {
|
|
8
|
-
"@prisma-next/contract": "0.5.0-dev.
|
|
9
|
-
"@prisma-next/
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/sql-
|
|
12
|
-
"@prisma-next/sql-
|
|
13
|
-
"@prisma-next/sql-
|
|
14
|
-
"@prisma-next/utils": "0.5.0-dev.
|
|
15
|
-
"@prisma-next/sql-
|
|
9
|
+
"@prisma-next/contract": "0.5.0-dev.61",
|
|
10
|
+
"@prisma-next/framework-components": "0.5.0-dev.61",
|
|
11
|
+
"@prisma-next/operations": "0.5.0-dev.61",
|
|
12
|
+
"@prisma-next/sql-operations": "0.5.0-dev.61",
|
|
13
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.61",
|
|
14
|
+
"@prisma-next/sql-contract": "0.5.0-dev.61",
|
|
15
|
+
"@prisma-next/utils": "0.5.0-dev.61",
|
|
16
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.61"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@types/pg": "8.16.0",
|
|
@@ -20,15 +21,14 @@
|
|
|
20
21
|
"tsdown": "0.18.4",
|
|
21
22
|
"typescript": "5.9.3",
|
|
22
23
|
"vitest": "4.0.17",
|
|
23
|
-
"@prisma-next/adapter-postgres": "0.5.0-dev.
|
|
24
|
-
"@prisma-next/
|
|
25
|
-
"@prisma-next/
|
|
26
|
-
"@prisma-next/
|
|
27
|
-
"@prisma-next/
|
|
28
|
-
"@prisma-next/
|
|
29
|
-
"@prisma-next/
|
|
30
|
-
"@prisma-next/target-postgres": "0.5.0-dev.
|
|
31
|
-
"@prisma-next/ids": "0.5.0-dev.6",
|
|
24
|
+
"@prisma-next/adapter-postgres": "0.5.0-dev.61",
|
|
25
|
+
"@prisma-next/cli": "0.5.0-dev.61",
|
|
26
|
+
"@prisma-next/driver-postgres": "0.5.0-dev.61",
|
|
27
|
+
"@prisma-next/extension-pgvector": "0.5.0-dev.61",
|
|
28
|
+
"@prisma-next/family-sql": "0.5.0-dev.61",
|
|
29
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.61",
|
|
30
|
+
"@prisma-next/ids": "0.5.0-dev.61",
|
|
31
|
+
"@prisma-next/target-postgres": "0.5.0-dev.61",
|
|
32
32
|
"@prisma-next/tsconfig": "0.0.0",
|
|
33
33
|
"@prisma-next/tsdown": "0.0.0",
|
|
34
34
|
"@prisma-next/test-utils": "0.0.1"
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection row dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Per-row decoding is performed upstream in `sql-runtime`'s row-yielding async
|
|
5
|
+
* generator (it `await`s `decodeRow` once per row before yielding). This file
|
|
6
|
+
* never calls codec query-time methods directly; it consumes plain decoded
|
|
7
|
+
* cells through `executeQueryPlan` → `scope.execute(plan)` →
|
|
8
|
+
* `AsyncIterableResult<Row>`. Every `for await` / `.toArray()` consumer below
|
|
9
|
+
* therefore sees plain `T` values, not `Promise<T>`.
|
|
10
|
+
*
|
|
11
|
+
* See `packages/2-sql/5-runtime/src/codecs/decoding.ts` for the decode-once-
|
|
12
|
+
* per-row contract; this file is the consumer side of that contract. See also
|
|
13
|
+
* ADR 030 (codecs registry & decode boundary) and the m3 coverage in
|
|
14
|
+
* `test/integration/codec-async.test.ts` and `test/codec-async.types.test-d.ts`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { AsyncIterableResult } from '@prisma-next/runtime
|
|
18
|
+
import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
3
19
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
20
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
4
21
|
import { isToOneCardinality, resolvePolymorphismInfo } from './collection-contract';
|
|
5
22
|
import {
|
|
6
23
|
acquireRuntimeScope,
|
|
@@ -25,7 +42,6 @@ import type {
|
|
|
25
42
|
IncludeExpr,
|
|
26
43
|
IncludeScalar,
|
|
27
44
|
RelationCardinalityTag,
|
|
28
|
-
RuntimeScope,
|
|
29
45
|
} from './types';
|
|
30
46
|
|
|
31
47
|
export function dispatchCollectionRows<Row>(options: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { AsyncIterableResult } from '@prisma-next/runtime
|
|
2
|
+
import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
3
3
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
4
|
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
5
5
|
import { stitchIncludes } from './collection-dispatch';
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { AsyncIterableResult } from '@prisma-next/runtime
|
|
2
|
+
import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
3
3
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
4
5
|
import {
|
|
5
6
|
getColumnToFieldMap,
|
|
6
7
|
getCompleteColumnToFieldMap,
|
|
7
8
|
getFieldToColumnMap,
|
|
8
9
|
type PolymorphismInfo,
|
|
9
10
|
} from './collection-contract';
|
|
10
|
-
import type { CollectionContext, RuntimeConnection
|
|
11
|
+
import type { CollectionContext, RuntimeConnection } from './types';
|
|
11
12
|
|
|
12
13
|
export interface RowEnvelope {
|
|
13
14
|
readonly raw: Record<string, unknown>;
|
package/src/collection.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { AsyncIterableResult } from '@prisma-next/runtime
|
|
2
|
+
import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
3
3
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
4
|
import {
|
|
5
5
|
BinaryExpr,
|
|
@@ -119,11 +119,17 @@ function applyCreateDefaults(
|
|
|
119
119
|
tableName: string,
|
|
120
120
|
rows: Record<string, unknown>[],
|
|
121
121
|
): void {
|
|
122
|
+
// Per-operation cache for generators with `stability: 'query'` (e.g.
|
|
123
|
+
// `timestampNow` for `temporal.updatedAt()`): one generated value
|
|
124
|
+
// shared across every row in this insert. Per-field generators
|
|
125
|
+
// (e.g. `cuid`) ignore the cache and vary per row.
|
|
126
|
+
const defaultValueCache = rows.length > 1 ? new Map<string, unknown>() : undefined;
|
|
122
127
|
for (const row of rows) {
|
|
123
128
|
const applied = ctx.context.applyMutationDefaults({
|
|
124
129
|
op: 'create',
|
|
125
130
|
table: tableName,
|
|
126
131
|
values: row,
|
|
132
|
+
...(defaultValueCache ? { defaultValueCache } : {}),
|
|
127
133
|
});
|
|
128
134
|
for (const def of applied) {
|
|
129
135
|
row[def.column] = def.value;
|
|
@@ -131,6 +137,21 @@ function applyCreateDefaults(
|
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
function applyUpdateDefaults(
|
|
141
|
+
ctx: CollectionContext<Contract<SqlStorage>>,
|
|
142
|
+
tableName: string,
|
|
143
|
+
values: Record<string, unknown>,
|
|
144
|
+
): void {
|
|
145
|
+
const applied = ctx.context.applyMutationDefaults({
|
|
146
|
+
op: 'update',
|
|
147
|
+
table: tableName,
|
|
148
|
+
values,
|
|
149
|
+
});
|
|
150
|
+
for (const def of applied) {
|
|
151
|
+
values[def.column] = def.value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
type WhereDirectInput = WhereArg;
|
|
135
156
|
|
|
136
157
|
function isToWhereExprInput(value: unknown): value is ToWhereExpr {
|
|
@@ -965,6 +986,9 @@ export class Collection<
|
|
|
965
986
|
applyCreateDefaults(this.ctx, this.tableName, [createValues]);
|
|
966
987
|
const updateValues = mapModelDataToStorageRow(this.contract, this.modelName, input.update);
|
|
967
988
|
const hasUpdateValues = Object.keys(updateValues).length > 0;
|
|
989
|
+
if (hasUpdateValues) {
|
|
990
|
+
applyUpdateDefaults(this.ctx, this.tableName, updateValues);
|
|
991
|
+
}
|
|
968
992
|
const conflictColumns = resolveUpsertConflictColumns(
|
|
969
993
|
this.contract,
|
|
970
994
|
this.modelName,
|
|
@@ -1057,6 +1081,8 @@ export class Collection<
|
|
|
1057
1081
|
return new AsyncIterableResult(generator());
|
|
1058
1082
|
}
|
|
1059
1083
|
|
|
1084
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
1085
|
+
|
|
1060
1086
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
1061
1087
|
const { selectedForQuery: selectedForUpdate, hiddenColumns } = augmentSelectionForJoinColumns(
|
|
1062
1088
|
this.state.selectedFields,
|
|
@@ -1088,6 +1114,8 @@ export class Collection<
|
|
|
1088
1114
|
return 0;
|
|
1089
1115
|
}
|
|
1090
1116
|
|
|
1117
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
1118
|
+
|
|
1091
1119
|
const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
|
|
1092
1120
|
const countState: CollectionState = {
|
|
1093
1121
|
...emptyState(),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
import type { RuntimeScope } from './types';
|
|
1
|
+
import type { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
2
|
+
import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
3
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
5
4
|
|
|
6
5
|
export function executeQueryPlan<Row>(
|
|
7
6
|
scope: RuntimeScope,
|
|
8
|
-
plan:
|
|
7
|
+
plan: SqlExecutionPlan<Row> | SqlQueryPlan<Row>,
|
|
9
8
|
): AsyncIterableResult<Row> {
|
|
10
9
|
return scope.execute(plan);
|
|
11
10
|
}
|
package/src/filters.ts
CHANGED
|
@@ -73,7 +73,7 @@ function assertFieldHasEqualityTrait(
|
|
|
73
73
|
): void {
|
|
74
74
|
const fieldType = modelOf(context.contract, modelName)?.fields?.[fieldName]?.type;
|
|
75
75
|
const codecId = fieldType?.kind === 'scalar' ? fieldType.codecId : undefined;
|
|
76
|
-
const traits = codecId ? context.
|
|
76
|
+
const traits = codecId ? (context.codecDescriptors.descriptorFor(codecId)?.traits ?? []) : [];
|
|
77
77
|
if (!traits.includes('equality')) {
|
|
78
78
|
throw new Error(
|
|
79
79
|
`Shorthand filter on "${modelName}.${fieldName}": field does not support equality comparisons`,
|
package/src/include-strategy.ts
CHANGED
|
@@ -3,10 +3,29 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
|
3
3
|
|
|
4
4
|
export type IncludeStrategy = 'lateral' | 'correlated' | 'multiQuery';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Choose the SQL emission strategy for nested includes based on the
|
|
8
|
+
* contract's declared capabilities.
|
|
9
|
+
*
|
|
10
|
+
* - `'lateral'`: outer SELECT with one LATERAL JOIN per relation,
|
|
11
|
+
* aggregating to JSON. Requires both `lateral` and `jsonAgg`.
|
|
12
|
+
* Postgres has both.
|
|
13
|
+
* - `'correlated'`: outer SELECT with one correlated subquery per
|
|
14
|
+
* relation, aggregating to JSON. Requires `jsonAgg` only.
|
|
15
|
+
* SQLite has `jsonAgg` (via `json_group_array`) but no LATERAL.
|
|
16
|
+
* - `'multiQuery'`: fallback. One SELECT per relation, stitched
|
|
17
|
+
* together in JS via `WHERE pk IN (parent-pk-values)`. Always
|
|
18
|
+
* correct; just N+1 round-trips.
|
|
19
|
+
*
|
|
20
|
+
* The capability flags are looked up under the contract's
|
|
21
|
+
* `targetFamily` and `target` namespaces — the two layers the contract
|
|
22
|
+
* emitter actually populates. Cross-namespace ("`postgres.lateral`
|
|
23
|
+
* found while running SQLite") false positives are impossible because
|
|
24
|
+
* we only inspect the running target's namespaces.
|
|
25
|
+
*/
|
|
6
26
|
export function selectIncludeStrategy(contract: Contract<SqlStorage>): IncludeStrategy {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const hasJsonAgg = hasCapability(capabilities?.['jsonAgg']);
|
|
27
|
+
const hasLateral = capabilityFlag(contract, 'lateral');
|
|
28
|
+
const hasJsonAgg = capabilityFlag(contract, 'jsonAgg');
|
|
10
29
|
|
|
11
30
|
if (hasLateral && hasJsonAgg) {
|
|
12
31
|
return 'lateral';
|
|
@@ -19,15 +38,18 @@ export function selectIncludeStrategy(contract: Contract<SqlStorage>): IncludeSt
|
|
|
19
38
|
return 'multiQuery';
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
41
|
+
/**
|
|
42
|
+
* Read a capability flag from the contract's target/family namespaces.
|
|
43
|
+
*
|
|
44
|
+
* The contract emitter populates `capabilities[targetFamily]` (universal
|
|
45
|
+
* SQL flags like `jsonAgg`, `returning`) and `capabilities[target]`
|
|
46
|
+
* (target-specific flags like `lateral` on Postgres). Either may
|
|
47
|
+
* declare a given flag; the family namespace declares the floor and the
|
|
48
|
+
* target namespace can extend on top.
|
|
49
|
+
*/
|
|
50
|
+
function capabilityFlag(contract: Contract<SqlStorage>, flag: string): boolean {
|
|
51
|
+
return (
|
|
52
|
+
contract.capabilities[contract.targetFamily]?.[flag] === true ||
|
|
53
|
+
contract.capabilities[contract.target]?.[flag] === true
|
|
54
|
+
);
|
|
33
55
|
}
|
package/src/model-accessor.ts
CHANGED
|
@@ -7,12 +7,11 @@ import {
|
|
|
7
7
|
BinaryExpr,
|
|
8
8
|
ColumnRef,
|
|
9
9
|
ExistsExpr,
|
|
10
|
-
OperationExpr,
|
|
11
|
-
ParamRef,
|
|
12
10
|
ProjectionItem,
|
|
13
11
|
SelectAst,
|
|
14
12
|
TableSource,
|
|
15
13
|
} from '@prisma-next/sql-relational-core/ast';
|
|
14
|
+
import type { Expression, ScopeField } from '@prisma-next/sql-relational-core/expression';
|
|
16
15
|
import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
17
16
|
import {
|
|
18
17
|
getFieldToColumnMap,
|
|
@@ -67,15 +66,16 @@ export function createModelAccessor<
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
for (const [name, entry] of Object.entries(context.queryOperations.entries())) {
|
|
70
|
-
const self = entry.args[0];
|
|
71
69
|
const op: NamedOp = [name, entry];
|
|
72
|
-
|
|
70
|
+
const self = entry.self;
|
|
71
|
+
if (!self) continue;
|
|
72
|
+
if (self.codecId !== undefined) {
|
|
73
73
|
registerOp(self.codecId, op);
|
|
74
|
-
} else if (self
|
|
75
|
-
for (const
|
|
76
|
-
const
|
|
77
|
-
if (self.traits.every((t) =>
|
|
78
|
-
registerOp(
|
|
74
|
+
} else if (self.traits !== undefined) {
|
|
75
|
+
for (const descriptor of context.codecDescriptors.values()) {
|
|
76
|
+
const descriptorTraits: readonly string[] = descriptor.traits;
|
|
77
|
+
if (self.traits.every((t) => descriptorTraits.includes(t))) {
|
|
78
|
+
registerOp(descriptor.codecId, op);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -93,96 +93,96 @@ export function createModelAccessor<
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const columnName = fieldToColumn[prop] ?? prop;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const column = resolveColumn(contract, tableName, columnName);
|
|
97
|
+
// Unknown fields return `undefined`, matching plain JS object semantics.
|
|
98
|
+
// The `ModelAccessor<TContract, ModelName>` type already rejects typos
|
|
99
|
+
// at compile time for TS consumers, and contexts that iterate accessor
|
|
100
|
+
// keys (e.g. relation-shorthand predicates) can detect missing fields
|
|
101
|
+
// with an `undefined` check and raise their own, domain-specific error.
|
|
102
|
+
if (!column) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const traits = context.codecDescriptors.descriptorFor(column.codecId)?.traits ?? [];
|
|
106
|
+
const operations = opsByCodecId.get(column.codecId) ?? [];
|
|
107
|
+
return createScalarFieldAccessor(
|
|
108
|
+
tableName,
|
|
109
|
+
columnName,
|
|
110
|
+
column.codecId,
|
|
111
|
+
column.nullable,
|
|
112
|
+
traits,
|
|
113
|
+
operations,
|
|
114
|
+
context,
|
|
115
|
+
);
|
|
100
116
|
},
|
|
101
117
|
});
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
function
|
|
105
|
-
contract: Contract<SqlStorage>,
|
|
106
|
-
modelName: string,
|
|
107
|
-
fieldName: string,
|
|
108
|
-
context: ExecutionContext,
|
|
109
|
-
): readonly string[] {
|
|
110
|
-
const fieldType = modelOf(contract, modelName)?.fields?.[fieldName]?.type;
|
|
111
|
-
const codecId = fieldType?.kind === 'scalar' ? fieldType.codecId : undefined;
|
|
112
|
-
if (!codecId) return [];
|
|
113
|
-
return context.codecs.traitsOf(codecId);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolveFieldCodecId(
|
|
120
|
+
function resolveColumn(
|
|
117
121
|
contract: Contract<SqlStorage>,
|
|
118
122
|
tableName: string,
|
|
119
123
|
columnName: string,
|
|
120
|
-
): string | undefined {
|
|
121
|
-
const
|
|
122
|
-
return
|
|
124
|
+
): { readonly codecId: string; readonly nullable: boolean } | undefined {
|
|
125
|
+
const column = contract.storage.tables?.[tableName]?.columns?.[columnName];
|
|
126
|
+
if (!column) return undefined;
|
|
127
|
+
return { codecId: column.codecId, nullable: column.nullable };
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
function createScalarFieldAccessor(
|
|
126
131
|
tableName: string,
|
|
127
132
|
columnName: string,
|
|
128
|
-
codecId: string
|
|
133
|
+
codecId: string,
|
|
134
|
+
nullable: boolean,
|
|
129
135
|
traits: readonly string[],
|
|
130
136
|
operations: readonly NamedOp[],
|
|
131
137
|
context: ExecutionContext,
|
|
132
138
|
): Partial<ComparisonMethodFns<unknown>> {
|
|
133
139
|
const column = ColumnRef.of(tableName, columnName);
|
|
134
|
-
const
|
|
135
|
-
|
|
140
|
+
const comparisonEntries: Array<[string, unknown]> = [];
|
|
136
141
|
for (const [name, meta] of Object.entries(COMPARISON_METHODS_META)) {
|
|
137
|
-
if (meta.traits.some((t) => !traits.includes(t)))
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
methods[name] = meta.create(column, codecId);
|
|
142
|
+
if (meta.traits.some((t) => !traits.includes(t))) continue;
|
|
143
|
+
comparisonEntries.push([name, meta.create(column, codecId)]);
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
const accessor = {
|
|
147
|
+
returnType: { codecId, nullable },
|
|
148
|
+
buildAst: () => column,
|
|
149
|
+
...Object.fromEntries(comparisonEntries),
|
|
150
|
+
} as Expression<ScopeField> & Record<string, unknown>;
|
|
151
|
+
|
|
143
152
|
for (const [name, entry] of operations) {
|
|
144
|
-
|
|
153
|
+
accessor[name] = createExtensionMethodFactory(accessor, entry, context);
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
return
|
|
156
|
+
return accessor as Partial<ComparisonMethodFns<unknown>>;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
function createExtensionMethodFactory(
|
|
151
|
-
|
|
152
|
-
methodName: string,
|
|
160
|
+
selfExpr: Expression<ScopeField>,
|
|
153
161
|
entry: SqlOperationEntry,
|
|
154
162
|
context: ExecutionContext,
|
|
155
163
|
): (...args: unknown[]) => unknown {
|
|
156
|
-
const returnTraits = context.codecs.traitsOf(entry.returns.codecId);
|
|
157
|
-
const isPredicate = returnTraits.includes('boolean');
|
|
158
|
-
|
|
159
164
|
return (...args: unknown[]) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
lowering: entry.lowering,
|
|
171
|
-
});
|
|
165
|
+
// `entry.impl` is typed `(...args: never[]) => QueryOperationReturn` —
|
|
166
|
+
// `never[]` args block direct invocation with unknown values, and the
|
|
167
|
+
// declared return omits `buildAst` (sql-contract intentionally doesn't
|
|
168
|
+
// depend on relational-core). Cast here to the practical shape: authors
|
|
169
|
+
// always return Expression<ScopeField> via `buildOperation`.
|
|
170
|
+
const impl = entry.impl as (self: unknown, ...args: unknown[]) => Expression<ScopeField>;
|
|
171
|
+
const result = impl(selfExpr, ...args);
|
|
172
|
+
const returnCodecId = result.returnType.codecId;
|
|
173
|
+
const returnTraits = context.codecDescriptors.descriptorFor(returnCodecId)?.traits ?? [];
|
|
174
|
+
const isPredicate = returnTraits.includes('boolean');
|
|
172
175
|
|
|
173
176
|
if (isPredicate) {
|
|
174
|
-
return
|
|
177
|
+
return result.buildAst();
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
const resultAst = result.buildAst();
|
|
177
181
|
const methods: Record<string, unknown> = {};
|
|
178
|
-
|
|
179
182
|
for (const [resultMethodName, meta] of Object.entries(COMPARISON_METHODS_META)) {
|
|
180
|
-
if (meta.traits.some((t) => !returnTraits.includes(t)))
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
methods[resultMethodName] = meta.create(opExpr, entry.returns.codecId);
|
|
183
|
+
if (meta.traits.some((t) => !returnTraits.includes(t))) continue;
|
|
184
|
+
methods[resultMethodName] = meta.create(resultAst, returnCodecId);
|
|
184
185
|
}
|
|
185
|
-
|
|
186
186
|
return methods;
|
|
187
187
|
};
|
|
188
188
|
}
|
|
@@ -293,8 +293,14 @@ function toRelationWhereExpr<TContract extends Contract<SqlStorage>>(
|
|
|
293
293
|
const fieldAccessor = (accessor as Record<string, Partial<ComparisonMethodFns<unknown>>>)[
|
|
294
294
|
fieldName
|
|
295
295
|
];
|
|
296
|
+
// Unknown field in the shorthand predicate — the Proxy returns undefined
|
|
297
|
+
// for fields the contract doesn't declare. Surface it explicitly: silent
|
|
298
|
+
// skip would drop user intent (e.g. a typo'd `nmae: 'Alice'` filter would
|
|
299
|
+
// match every row).
|
|
296
300
|
if (!fieldAccessor) {
|
|
297
|
-
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Shorthand filter on "${relatedModelName}.${fieldName}": field is not defined on the model`,
|
|
303
|
+
);
|
|
298
304
|
}
|
|
299
305
|
|
|
300
306
|
if (value === null) {
|
package/src/mutation-executor.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
LiteralExpr,
|
|
8
8
|
} from '@prisma-next/sql-relational-core/ast';
|
|
9
9
|
import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context';
|
|
10
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
10
11
|
import {
|
|
11
12
|
getColumnToFieldMap,
|
|
12
13
|
resolveFieldToColumn,
|
|
@@ -40,7 +41,6 @@ import type {
|
|
|
40
41
|
RelationMutation,
|
|
41
42
|
RelationMutator,
|
|
42
43
|
RuntimeQueryable,
|
|
43
|
-
RuntimeScope,
|
|
44
44
|
} from './types';
|
|
45
45
|
import { emptyState } from './types';
|
|
46
46
|
|
|
@@ -236,6 +236,15 @@ async function updateFirstGraph(
|
|
|
236
236
|
|
|
237
237
|
const mappedUpdateData = mapModelDataToStorageRow(contract, modelName, scalarData);
|
|
238
238
|
if (Object.keys(mappedUpdateData).length > 0) {
|
|
239
|
+
const tableName = resolveModelTableName(contract, modelName);
|
|
240
|
+
const appliedUpdateDefaults = context.applyMutationDefaults({
|
|
241
|
+
op: 'update',
|
|
242
|
+
table: tableName,
|
|
243
|
+
values: mappedUpdateData,
|
|
244
|
+
});
|
|
245
|
+
for (const def of appliedUpdateDefaults) {
|
|
246
|
+
mappedUpdateData[def.column] = def.value;
|
|
247
|
+
}
|
|
239
248
|
const pkFilter = buildPrimaryKeyFilterFromRow(contract, modelName, existingRow);
|
|
240
249
|
const pkWhere = shorthandToWhereExpr(
|
|
241
250
|
context,
|
|
@@ -246,7 +255,6 @@ async function updateFirstGraph(
|
|
|
246
255
|
throw new Error(`Failed to build primary key filter for model "${modelName}"`);
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
const tableName = resolveModelTableName(contract, modelName);
|
|
250
258
|
const compiled = compileUpdateReturning(
|
|
251
259
|
contract,
|
|
252
260
|
tableName,
|
|
@@ -18,16 +18,30 @@ import { buildOrmQueryPlan, deriveParamsFromAst } from './query-plan-meta';
|
|
|
18
18
|
import type { AggregateSelector } from './types';
|
|
19
19
|
import { combineWhereExprs } from './where-utils';
|
|
20
20
|
|
|
21
|
-
function
|
|
21
|
+
function toAggregateProjection(
|
|
22
|
+
contract: Contract<SqlStorage>,
|
|
23
|
+
tableName: string,
|
|
24
|
+
selector: AggregateSelector<unknown>,
|
|
25
|
+
): { expr: AggregateExpr; codecId: string | undefined } {
|
|
22
26
|
if (selector.fn === 'count') {
|
|
23
|
-
|
|
27
|
+
// count() returns a target-specific bigint; mapping isn't derivable here
|
|
28
|
+
// without target coupling, so we leave codecId unstamped.
|
|
29
|
+
return { expr: AggregateExpr.count(), codecId: undefined };
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
if (!selector.column) {
|
|
27
33
|
throw new Error(`Aggregate selector "${selector.fn}" requires a field`);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
const expr = new AggregateExpr(selector.fn, ColumnRef.of(tableName, selector.column));
|
|
37
|
+
// min/max preserve the input column's type, so propagate the column codec.
|
|
38
|
+
// sum widens (int4 → int8 in Postgres) and avg → numeric; both need
|
|
39
|
+
// target+input-aware mapping that doesn't exist yet, so leave unstamped.
|
|
40
|
+
if (selector.fn === 'min' || selector.fn === 'max') {
|
|
41
|
+
const codecId = contract.storage.tables[tableName]?.columns[selector.column]?.codecId;
|
|
42
|
+
return { expr, codecId };
|
|
43
|
+
}
|
|
44
|
+
return { expr, codecId: undefined };
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
// ORM HAVING filters use literal binding (values inlined at plan-build time),
|
|
@@ -115,17 +129,18 @@ export function compileAggregate(
|
|
|
115
129
|
throw new Error('aggregate() requires at least one aggregation selector');
|
|
116
130
|
}
|
|
117
131
|
|
|
118
|
-
const projection: ProjectionItem[] = entries.map(([alias, selector]) =>
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
const projection: ProjectionItem[] = entries.map(([alias, selector]) => {
|
|
133
|
+
const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
|
|
134
|
+
return ProjectionItem.of(alias, expr, codecId);
|
|
135
|
+
});
|
|
121
136
|
let ast = SelectAst.from(TableSource.named(tableName)).withProjection(projection);
|
|
122
137
|
const where = combineWhereExprs(filters);
|
|
123
138
|
if (where) {
|
|
124
139
|
ast = ast.withWhere(where);
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
const { params
|
|
128
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
142
|
+
const { params } = deriveParamsFromAst(ast);
|
|
143
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
export function compileGroupedAggregate(
|
|
@@ -145,11 +160,15 @@ export function compileGroupedAggregate(
|
|
|
145
160
|
throw new Error('groupBy().aggregate() requires at least one aggregation selector');
|
|
146
161
|
}
|
|
147
162
|
|
|
163
|
+
const table = contract.storage.tables[tableName];
|
|
148
164
|
const projection: ProjectionItem[] = [
|
|
149
|
-
...groupByColumns.map((column) =>
|
|
150
|
-
|
|
151
|
-
ProjectionItem.of(alias, toAggregateExpr(tableName, selector)),
|
|
165
|
+
...groupByColumns.map((column) =>
|
|
166
|
+
ProjectionItem.of(column, ColumnRef.of(tableName, column), table?.columns[column]?.codecId),
|
|
152
167
|
),
|
|
168
|
+
...entries.map(([alias, selector]) => {
|
|
169
|
+
const { expr, codecId } = toAggregateProjection(contract, tableName, selector);
|
|
170
|
+
return ProjectionItem.of(alias, expr, codecId);
|
|
171
|
+
}),
|
|
153
172
|
];
|
|
154
173
|
|
|
155
174
|
let ast = SelectAst.from(TableSource.named(tableName))
|
|
@@ -164,6 +183,6 @@ export function compileGroupedAggregate(
|
|
|
164
183
|
ast = ast.withHaving(validateGroupedHavingExpr(havingExpr));
|
|
165
184
|
}
|
|
166
185
|
|
|
167
|
-
const { params
|
|
168
|
-
return buildOrmQueryPlan(contract, ast, params
|
|
186
|
+
const { params } = deriveParamsFromAst(ast);
|
|
187
|
+
return buildOrmQueryPlan(contract, ast, params);
|
|
169
188
|
}
|