@prisma-next/sql-orm-client 0.5.0-dev.8 → 0.5.0-dev.87
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 +11 -1
- package/dist/index.d.mts +53 -45
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +310 -214
- package/dist/index.mjs.map +1 -1
- package/package.json +24 -26
- package/src/collection-contract.ts +19 -0
- package/src/collection-dispatch.ts +2 -2
- package/src/collection-mutation-dispatch.ts +1 -1
- package/src/collection-runtime.ts +3 -2
- package/src/collection.ts +104 -9
- 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,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/sql-orm-client",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.87",
|
|
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/
|
|
9
|
-
"@prisma-next/
|
|
10
|
-
"@prisma-next/
|
|
11
|
-
"@prisma-next/
|
|
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.87",
|
|
10
|
+
"@prisma-next/framework-components": "0.5.0-dev.87",
|
|
11
|
+
"@prisma-next/operations": "0.5.0-dev.87",
|
|
12
|
+
"@prisma-next/sql-contract": "0.5.0-dev.87",
|
|
13
|
+
"@prisma-next/sql-operations": "0.5.0-dev.87",
|
|
14
|
+
"@prisma-next/sql-relational-core": "0.5.0-dev.87",
|
|
15
|
+
"@prisma-next/utils": "0.5.0-dev.87",
|
|
16
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.87"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
|
-
"@types/pg": "8.
|
|
19
|
-
"pg": "8.
|
|
20
|
-
"tsdown": "0.
|
|
19
|
+
"@types/pg": "8.20.0",
|
|
20
|
+
"pg": "8.20.0",
|
|
21
|
+
"tsdown": "0.22.0",
|
|
21
22
|
"typescript": "5.9.3",
|
|
22
|
-
"vitest": "4.
|
|
23
|
-
"@prisma-next/adapter-postgres": "0.5.0-dev.
|
|
24
|
-
"@prisma-next/
|
|
25
|
-
"@prisma-next/driver-postgres": "0.5.0-dev.
|
|
26
|
-
"@prisma-next/
|
|
27
|
-
"@prisma-next/
|
|
28
|
-
"@prisma-next/ids": "0.5.0-dev.
|
|
29
|
-
"@prisma-next/
|
|
30
|
-
"@prisma-next/
|
|
31
|
-
"@prisma-next/target-postgres": "0.5.0-dev.8",
|
|
23
|
+
"vitest": "4.1.5",
|
|
24
|
+
"@prisma-next/adapter-postgres": "0.5.0-dev.87",
|
|
25
|
+
"@prisma-next/extension-pgvector": "0.5.0-dev.87",
|
|
26
|
+
"@prisma-next/driver-postgres": "0.5.0-dev.87",
|
|
27
|
+
"@prisma-next/family-sql": "0.5.0-dev.87",
|
|
28
|
+
"@prisma-next/cli": "0.5.0-dev.87",
|
|
29
|
+
"@prisma-next/ids": "0.5.0-dev.87",
|
|
30
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.87",
|
|
31
|
+
"@prisma-next/target-postgres": "0.5.0-dev.87",
|
|
32
32
|
"@prisma-next/test-utils": "0.0.1",
|
|
33
|
-
"@prisma-next/
|
|
34
|
-
"@prisma-next/
|
|
33
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
34
|
+
"@prisma-next/tsconfig": "0.0.0"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
@@ -44,8 +44,6 @@
|
|
|
44
44
|
".": "./dist/index.mjs",
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
|
-
"main": "./dist/index.mjs",
|
|
48
|
-
"module": "./dist/index.mjs",
|
|
49
47
|
"types": "./dist/index.d.mts",
|
|
50
48
|
"repository": {
|
|
51
49
|
"type": "git",
|
|
@@ -322,6 +322,25 @@ export function resolvePrimaryKeyColumn(contract: Contract<SqlStorage>, tableNam
|
|
|
322
322
|
return contract.storage.tables[tableName]?.primaryKey?.columns[0] ?? 'id';
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
export function resolveRowIdentityColumns(
|
|
326
|
+
contract: Contract<SqlStorage>,
|
|
327
|
+
tableName: string,
|
|
328
|
+
): readonly string[] {
|
|
329
|
+
const table = contract.storage.tables[tableName];
|
|
330
|
+
if (!table) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
if (table.primaryKey && table.primaryKey.columns.length > 0) {
|
|
334
|
+
return table.primaryKey.columns;
|
|
335
|
+
}
|
|
336
|
+
for (const unique of table.uniques) {
|
|
337
|
+
if (unique.columns.length > 0) {
|
|
338
|
+
return unique.columns;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
|
|
325
344
|
export function assertReturningCapability(contract: Contract<SqlStorage>, action: string): void {
|
|
326
345
|
if (hasContractCapability(contract, 'returning')) {
|
|
327
346
|
return;
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Contract } from '@prisma-next/contract/types';
|
|
18
|
-
import { AsyncIterableResult } from '@prisma-next/runtime
|
|
18
|
+
import { AsyncIterableResult } from '@prisma-next/framework-components/runtime';
|
|
19
19
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
20
|
+
import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
|
|
20
21
|
import { isToOneCardinality, resolvePolymorphismInfo } from './collection-contract';
|
|
21
22
|
import {
|
|
22
23
|
acquireRuntimeScope,
|
|
@@ -41,7 +42,6 @@ import type {
|
|
|
41
42
|
IncludeExpr,
|
|
42
43
|
IncludeScalar,
|
|
43
44
|
RelationCardinalityTag,
|
|
44
|
-
RuntimeScope,
|
|
45
45
|
} from './types';
|
|
46
46
|
|
|
47
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,7 +1,8 @@
|
|
|
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
|
+
type AnyExpression,
|
|
5
6
|
BinaryExpr,
|
|
6
7
|
ColumnRef,
|
|
7
8
|
isWhereExpr,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
resolveModelTableName,
|
|
26
27
|
resolvePolymorphismInfo,
|
|
27
28
|
resolvePrimaryKeyColumn,
|
|
29
|
+
resolveRowIdentityColumns,
|
|
28
30
|
resolveUpsertConflictColumns,
|
|
29
31
|
} from './collection-contract';
|
|
30
32
|
import { dispatchCollectionRows } from './collection-dispatch';
|
|
@@ -107,6 +109,7 @@ import {
|
|
|
107
109
|
type RelatedModelName,
|
|
108
110
|
type RelationNames,
|
|
109
111
|
type ResolvedCreateInput,
|
|
112
|
+
type RuntimeQueryable,
|
|
110
113
|
type ShorthandWhereFilter,
|
|
111
114
|
type UniqueConstraintCriterion,
|
|
112
115
|
type VariantModelRow,
|
|
@@ -119,11 +122,17 @@ function applyCreateDefaults(
|
|
|
119
122
|
tableName: string,
|
|
120
123
|
rows: Record<string, unknown>[],
|
|
121
124
|
): void {
|
|
125
|
+
// Per-operation cache for generators with `stability: 'query'` (e.g.
|
|
126
|
+
// `timestampNow` for `temporal.updatedAt()`): one generated value
|
|
127
|
+
// shared across every row in this insert. Per-field generators
|
|
128
|
+
// (e.g. `cuid`) ignore the cache and vary per row.
|
|
129
|
+
const defaultValueCache = rows.length > 1 ? new Map<string, unknown>() : undefined;
|
|
122
130
|
for (const row of rows) {
|
|
123
131
|
const applied = ctx.context.applyMutationDefaults({
|
|
124
132
|
op: 'create',
|
|
125
133
|
table: tableName,
|
|
126
134
|
values: row,
|
|
135
|
+
...(defaultValueCache ? { defaultValueCache } : {}),
|
|
127
136
|
});
|
|
128
137
|
for (const def of applied) {
|
|
129
138
|
row[def.column] = def.value;
|
|
@@ -131,6 +140,21 @@ function applyCreateDefaults(
|
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
function applyUpdateDefaults(
|
|
144
|
+
ctx: CollectionContext<Contract<SqlStorage>>,
|
|
145
|
+
tableName: string,
|
|
146
|
+
values: Record<string, unknown>,
|
|
147
|
+
): void {
|
|
148
|
+
const applied = ctx.context.applyMutationDefaults({
|
|
149
|
+
op: 'update',
|
|
150
|
+
table: tableName,
|
|
151
|
+
values,
|
|
152
|
+
});
|
|
153
|
+
for (const def of applied) {
|
|
154
|
+
values[def.column] = def.value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
type WhereDirectInput = WhereArg;
|
|
135
159
|
|
|
136
160
|
function isToWhereExprInput(value: unknown): value is ToWhereExpr {
|
|
@@ -965,6 +989,9 @@ export class Collection<
|
|
|
965
989
|
applyCreateDefaults(this.ctx, this.tableName, [createValues]);
|
|
966
990
|
const updateValues = mapModelDataToStorageRow(this.contract, this.modelName, input.update);
|
|
967
991
|
const hasUpdateValues = Object.keys(updateValues).length > 0;
|
|
992
|
+
if (hasUpdateValues) {
|
|
993
|
+
applyUpdateDefaults(this.ctx, this.tableName, updateValues);
|
|
994
|
+
}
|
|
968
995
|
const conflictColumns = resolveUpsertConflictColumns(
|
|
969
996
|
this.contract,
|
|
970
997
|
this.modelName,
|
|
@@ -1038,12 +1065,20 @@ export class Collection<
|
|
|
1038
1065
|
return this.#reloadMutationRowByPrimaryKey(pkCriterion);
|
|
1039
1066
|
}
|
|
1040
1067
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1068
|
+
return withMutationScope(this.ctx.runtime, async (scope) => {
|
|
1069
|
+
const scoped = this.#withRuntime(scope);
|
|
1070
|
+
const identityWhere = await scoped.#findFirstMatchingRowIdentityWhere();
|
|
1071
|
+
if (!identityWhere) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
const narrowed = scoped.#clone({ filters: [identityWhere] });
|
|
1075
|
+
const rows = await narrowed.updateAll(
|
|
1076
|
+
data as State['hasWhere'] extends true
|
|
1077
|
+
? Partial<DefaultModelRow<TContract, ModelName>>
|
|
1078
|
+
: never,
|
|
1079
|
+
);
|
|
1080
|
+
return rows[0] ?? null;
|
|
1081
|
+
});
|
|
1047
1082
|
}
|
|
1048
1083
|
|
|
1049
1084
|
updateAll(
|
|
@@ -1057,6 +1092,8 @@ export class Collection<
|
|
|
1057
1092
|
return new AsyncIterableResult(generator());
|
|
1058
1093
|
}
|
|
1059
1094
|
|
|
1095
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
1096
|
+
|
|
1060
1097
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
1061
1098
|
const { selectedForQuery: selectedForUpdate, hiddenColumns } = augmentSelectionForJoinColumns(
|
|
1062
1099
|
this.state.selectedFields,
|
|
@@ -1088,6 +1125,8 @@ export class Collection<
|
|
|
1088
1125
|
return 0;
|
|
1089
1126
|
}
|
|
1090
1127
|
|
|
1128
|
+
applyUpdateDefaults(this.ctx, this.tableName, mappedData);
|
|
1129
|
+
|
|
1091
1130
|
const primaryKeyColumn = resolvePrimaryKeyColumn(this.contract, this.tableName);
|
|
1092
1131
|
const countState: CollectionState = {
|
|
1093
1132
|
...emptyState(),
|
|
@@ -1115,15 +1154,26 @@ export class Collection<
|
|
|
1115
1154
|
this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
|
|
1116
1155
|
): Promise<Row | null> {
|
|
1117
1156
|
assertReturningCapability(this.contract, 'delete()');
|
|
1118
|
-
|
|
1119
|
-
|
|
1157
|
+
return withMutationScope(this.ctx.runtime, async (scope) => {
|
|
1158
|
+
const scoped = this.#withRuntime(scope);
|
|
1159
|
+
const identityWhere = await scoped.#findFirstMatchingRowIdentityWhere();
|
|
1160
|
+
if (!identityWhere) {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
const narrowed = scoped.#clone({ filters: [identityWhere] });
|
|
1164
|
+
const rows = await narrowed.#executeDeleteReturning().toArray();
|
|
1165
|
+
return rows[0] ?? null;
|
|
1166
|
+
});
|
|
1120
1167
|
}
|
|
1121
1168
|
|
|
1122
1169
|
deleteAll(
|
|
1123
1170
|
this: State['hasWhere'] extends true ? Collection<TContract, ModelName, Row, State> : never,
|
|
1124
1171
|
): AsyncIterableResult<Row> {
|
|
1125
1172
|
assertReturningCapability(this.contract, 'deleteAll()');
|
|
1173
|
+
return this.#executeDeleteReturning();
|
|
1174
|
+
}
|
|
1126
1175
|
|
|
1176
|
+
#executeDeleteReturning(): AsyncIterableResult<Row> {
|
|
1127
1177
|
const parentJoinColumns = this.state.includes.map((include) => include.localColumn);
|
|
1128
1178
|
const { selectedForQuery: selectedForDelete, hiddenColumns } = augmentSelectionForJoinColumns(
|
|
1129
1179
|
this.state.selectedFields,
|
|
@@ -1188,6 +1238,41 @@ export class Collection<
|
|
|
1188
1238
|
return criterion;
|
|
1189
1239
|
}
|
|
1190
1240
|
|
|
1241
|
+
async #findFirstMatchingRowIdentityWhere(): Promise<AnyExpression | null> {
|
|
1242
|
+
const identityColumns = resolveRowIdentityColumns(this.contract, this.tableName);
|
|
1243
|
+
if (identityColumns.length === 0) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`update()/delete() on model "${this.modelName}" requires the table to have a primary key or unique constraint`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const firstRow = await this.#clone({
|
|
1249
|
+
selectedFields: [...identityColumns],
|
|
1250
|
+
includes: [],
|
|
1251
|
+
}).first();
|
|
1252
|
+
if (!firstRow) {
|
|
1253
|
+
return null;
|
|
1254
|
+
}
|
|
1255
|
+
const columnToField = getColumnToFieldMap(this.contract, this.modelName);
|
|
1256
|
+
const criterion: Record<string, unknown> = {};
|
|
1257
|
+
for (const column of identityColumns) {
|
|
1258
|
+
const fieldName = columnToField[column] ?? column;
|
|
1259
|
+
const value = (firstRow as Record<string, unknown>)[fieldName];
|
|
1260
|
+
if (value === undefined) {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`Missing identity field "${fieldName}" while resolving single-row scope for model "${this.modelName}"`,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
criterion[fieldName] = value;
|
|
1266
|
+
}
|
|
1267
|
+
return (
|
|
1268
|
+
shorthandToWhereExpr(
|
|
1269
|
+
this.ctx.context,
|
|
1270
|
+
this.modelName,
|
|
1271
|
+
criterion as ShorthandWhereFilter<TContract, ModelName>,
|
|
1272
|
+
) ?? null
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1191
1276
|
async #reloadMutationRowByPrimaryKey(criterion: Record<string, unknown>): Promise<Row | null> {
|
|
1192
1277
|
return this.#reloadMutationRowByCriterion(criterion, 'primary key');
|
|
1193
1278
|
}
|
|
@@ -1242,6 +1327,16 @@ export class Collection<
|
|
|
1242
1327
|
});
|
|
1243
1328
|
}
|
|
1244
1329
|
|
|
1330
|
+
#withRuntime(runtime: RuntimeQueryable): Collection<TContract, ModelName, Row, State> {
|
|
1331
|
+
const Ctor = this.constructor as CollectionConstructor<TContract>;
|
|
1332
|
+
return new Ctor({ ...this.ctx, runtime }, this.modelName, {
|
|
1333
|
+
tableName: this.tableName,
|
|
1334
|
+
state: this.state,
|
|
1335
|
+
registry: this.registry,
|
|
1336
|
+
includeRefinementMode: this.includeRefinementMode,
|
|
1337
|
+
}) as unknown as Collection<TContract, ModelName, Row, State>;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1245
1340
|
#cloneWithRow<NextRow, NextState extends CollectionTypeState = State>(
|
|
1246
1341
|
overrides: Partial<CollectionState>,
|
|
1247
1342
|
): Collection<TContract, ModelName, NextRow, NextState> {
|
|
@@ -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) {
|