@prisma-next/family-sql 0.13.0-dev.3 → 0.13.0-dev.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-type-constructors-CjFfO6LM.mjs +342 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +1 -0
- package/dist/{control-adapter-CgIL9Vtx.d.mts → control-adapter-Cmw9LvEP.d.mts} +16 -33
- package/dist/control-adapter-Cmw9LvEP.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +2 -2
- package/dist/control.d.mts +36 -34
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +24 -77
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +3 -2
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +2 -1
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.d.mts +16 -3
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +4 -2
- package/dist/pack.mjs.map +1 -1
- package/dist/schema-verify.d.mts +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{sql-contract-serializer-CY7qnms7.mjs → sql-contract-serializer-D6-28zKd.mjs} +26 -15
- package/dist/sql-contract-serializer-D6-28zKd.mjs.map +1 -0
- package/dist/{types-CbwQCzXY.d.mts → types-kgstZ_Zd.d.mts} +5 -5
- package/dist/types-kgstZ_Zd.d.mts.map +1 -0
- package/dist/{verify-sql-schema-DcMaT5Zj.d.mts → verify-sql-schema-thU-jKpf.d.mts} +2 -14
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +1 -0
- package/dist/{verify-sql-schema-DlAgBiT_.mjs → verify-sql-schema-xT4udQLQ.mjs} +25 -118
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +1 -0
- package/package.json +21 -21
- package/src/core/authoring-entity-types.ts +178 -0
- package/src/core/authoring-field-presets.ts +8 -3
- package/src/core/control-adapter.ts +18 -49
- package/src/core/control-descriptor.ts +3 -0
- package/src/core/control-instance.ts +13 -11
- package/src/core/ir/sql-contract-serializer-base.ts +76 -60
- package/src/core/migrations/contract-to-schema-ir.ts +47 -112
- package/src/core/migrations/types.ts +4 -1
- package/src/core/psl-contract-infer/postgres-type-map.ts +5 -13
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +17 -70
- package/src/core/schema-verify/verify-sql-schema.ts +10 -146
- package/src/core/sql-migration.ts +5 -1
- package/src/exports/control-adapter.ts +1 -0
- package/src/exports/control.ts +1 -1
- package/src/exports/pack.ts +3 -0
- package/dist/authoring-type-constructors-D4lQ-qpj.mjs +0 -192
- package/dist/authoring-type-constructors-D4lQ-qpj.mjs.map +0 -1
- package/dist/control-adapter-CgIL9Vtx.d.mts.map +0 -1
- package/dist/sql-contract-serializer-CY7qnms7.mjs.map +0 -1
- package/dist/types-CbwQCzXY.d.mts.map +0 -1
- package/dist/verify-sql-schema-DcMaT5Zj.d.mts.map +0 -1
- package/dist/verify-sql-schema-DlAgBiT_.mjs.map +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { JsonValue } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
AuthoringEntityContext,
|
|
4
|
+
AuthoringEntityTypeDescriptor,
|
|
5
|
+
AuthoringEntityTypeNamespace,
|
|
6
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
7
|
+
PslExtensionBlock,
|
|
8
|
+
} from '@prisma-next/framework-components/authoring';
|
|
9
|
+
import { type EnumTypeHandle, enumType } from '@prisma-next/sql-contract-ts/contract-builder';
|
|
10
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
11
|
+
|
|
12
|
+
function parseQuotedString(raw: string): string | undefined {
|
|
13
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
|
|
14
|
+
return raw.slice(1, -1);
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const sqlFamilyEnumEntityDescriptor = {
|
|
20
|
+
kind: 'entity' as const,
|
|
21
|
+
discriminator: 'enum',
|
|
22
|
+
output: {
|
|
23
|
+
factory: (
|
|
24
|
+
block: PslExtensionBlock,
|
|
25
|
+
ctx: AuthoringEntityContext,
|
|
26
|
+
): EnumTypeHandle | undefined => {
|
|
27
|
+
const sourceId = ctx.sourceId ?? 'unknown';
|
|
28
|
+
const diagnostics = ctx.diagnostics;
|
|
29
|
+
|
|
30
|
+
const typeAttr = block.blockAttributes.find((a) => a.name === 'type');
|
|
31
|
+
if (!typeAttr) {
|
|
32
|
+
diagnostics?.push({
|
|
33
|
+
code: 'PSL_ENUM_MISSING_TYPE',
|
|
34
|
+
message: `enum "${block.name}" is missing a @@type("codecId") attribute`,
|
|
35
|
+
sourceId,
|
|
36
|
+
span: block.span,
|
|
37
|
+
});
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rawCodecArg = typeAttr.args[0]?.value;
|
|
42
|
+
const codecId = rawCodecArg !== undefined ? parseQuotedString(rawCodecArg) : undefined;
|
|
43
|
+
if (!codecId) {
|
|
44
|
+
diagnostics?.push({
|
|
45
|
+
code: 'PSL_ENUM_MISSING_TYPE',
|
|
46
|
+
message: `enum "${block.name}" @@type attribute must have a quoted codec id argument`,
|
|
47
|
+
sourceId,
|
|
48
|
+
span: typeAttr.span,
|
|
49
|
+
});
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const nativeType = ctx.codecLookup?.targetTypesFor(codecId)?.[0];
|
|
54
|
+
if (nativeType === undefined) {
|
|
55
|
+
const typeArgSpan = typeAttr.args[0]?.span ?? typeAttr.span;
|
|
56
|
+
diagnostics?.push({
|
|
57
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
58
|
+
message: `enum "${block.name}" @@type references unknown codec "${codecId}"`,
|
|
59
|
+
sourceId,
|
|
60
|
+
span: typeArgSpan,
|
|
61
|
+
});
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const codec = ctx.codecLookup?.get(codecId);
|
|
66
|
+
if (codec === undefined) {
|
|
67
|
+
const typeArgSpan = typeAttr.args[0]?.span ?? typeAttr.span;
|
|
68
|
+
diagnostics?.push({
|
|
69
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
70
|
+
message: `enum "${block.name}" @@type codec "${codecId}" resolves in targetTypesFor but is absent from codecLookup.get`,
|
|
71
|
+
sourceId,
|
|
72
|
+
span: typeArgSpan,
|
|
73
|
+
});
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const seenValues = new Set<string>();
|
|
78
|
+
const members: { name: string; value: unknown }[] = [];
|
|
79
|
+
let memberError = false;
|
|
80
|
+
|
|
81
|
+
for (const [memberName, paramValue] of Object.entries(block.parameters)) {
|
|
82
|
+
let value: unknown;
|
|
83
|
+
if (paramValue.kind === 'bare') {
|
|
84
|
+
try {
|
|
85
|
+
value = codec.decodeJson(memberName);
|
|
86
|
+
} catch {
|
|
87
|
+
diagnostics?.push({
|
|
88
|
+
code: 'PSL_ENUM_BARE_MEMBER_NON_STRING_CODEC',
|
|
89
|
+
message: `enum "${block.name}" member "${memberName}" has no value and codec "${codecId}" does not accept a bare name as input`,
|
|
90
|
+
sourceId,
|
|
91
|
+
span: paramValue.span,
|
|
92
|
+
});
|
|
93
|
+
memberError = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
} else if (paramValue.kind === 'value') {
|
|
97
|
+
let jsonValue: unknown;
|
|
98
|
+
try {
|
|
99
|
+
jsonValue = JSON.parse(paramValue.raw);
|
|
100
|
+
} catch {
|
|
101
|
+
diagnostics?.push({
|
|
102
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
103
|
+
message: `enum "${block.name}" member "${memberName}" value "${paramValue.raw}" is not valid JSON`,
|
|
104
|
+
sourceId,
|
|
105
|
+
span: paramValue.span,
|
|
106
|
+
});
|
|
107
|
+
memberError = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
value = codec.decodeJson(
|
|
112
|
+
blindCast<JsonValue, 'JSON.parse returns a JsonValue-compatible value'>(jsonValue),
|
|
113
|
+
);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
116
|
+
diagnostics?.push({
|
|
117
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
118
|
+
message: `enum "${block.name}" member "${memberName}" was rejected by codec "${codecId}": ${reason}`,
|
|
119
|
+
sourceId,
|
|
120
|
+
span: paramValue.span,
|
|
121
|
+
});
|
|
122
|
+
memberError = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const valueKey = String(value);
|
|
130
|
+
if (seenValues.has(valueKey)) {
|
|
131
|
+
diagnostics?.push({
|
|
132
|
+
code: 'PSL_ENUM_DUPLICATE_MEMBER_VALUE',
|
|
133
|
+
message: `enum "${block.name}": duplicate member value "${valueKey}"`,
|
|
134
|
+
sourceId,
|
|
135
|
+
span: paramValue.span,
|
|
136
|
+
});
|
|
137
|
+
memberError = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
seenValues.add(valueKey);
|
|
141
|
+
members.push({ name: memberName, value });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (memberError) return undefined;
|
|
145
|
+
|
|
146
|
+
if (members.length === 0) {
|
|
147
|
+
diagnostics?.push({
|
|
148
|
+
code: 'PSL_ENUM_MISSING_TYPE',
|
|
149
|
+
message: `enum "${block.name}" must have at least one member`,
|
|
150
|
+
sourceId,
|
|
151
|
+
span: block.span,
|
|
152
|
+
});
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return enumType(
|
|
157
|
+
block.name,
|
|
158
|
+
{ codecId, nativeType },
|
|
159
|
+
...members.map((m) => ({ name: m.name, value: m.value })),
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
} satisfies AuthoringEntityTypeDescriptor;
|
|
164
|
+
|
|
165
|
+
export const sqlFamilyEntityTypes: AuthoringEntityTypeNamespace = {
|
|
166
|
+
enum: sqlFamilyEnumEntityDescriptor,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const sqlFamilyPslBlockDescriptors = {
|
|
170
|
+
enum: {
|
|
171
|
+
kind: 'pslBlock',
|
|
172
|
+
keyword: 'enum',
|
|
173
|
+
discriminator: 'enum',
|
|
174
|
+
name: { required: true },
|
|
175
|
+
parameters: {},
|
|
176
|
+
variadicParameters: true,
|
|
177
|
+
},
|
|
178
|
+
} as const satisfies AuthoringPslBlockDescriptorNamespace;
|
|
@@ -9,6 +9,11 @@ import type { AuthoringFieldNamespace } from '@prisma-next/framework-components/
|
|
|
9
9
|
* (`character`) regardless of target, and the PSL interpreter lets the
|
|
10
10
|
* generator override the scalar descriptor.
|
|
11
11
|
*
|
|
12
|
+
* The `uuidString` / `id.uuidv4String` / `id.uuidv7String` presets store UUID
|
|
13
|
+
* values as `character(36)` — portable across all SQL targets. For a native
|
|
14
|
+
* Postgres `uuid` column use `uuidNative` / `id.uuidv4Native` /
|
|
15
|
+
* `id.uuidv7Native` from `@prisma-next/target-postgres`.
|
|
16
|
+
*
|
|
12
17
|
* Scalar presets that map to target-specific codecs (e.g. `text`, `int`,
|
|
13
18
|
* `boolean`, `dateTime`) are contributed by the target pack (see
|
|
14
19
|
* `postgresAuthoringFieldPresets` in `@prisma-next/target-postgres`) so the
|
|
@@ -34,7 +39,7 @@ const nanoidOptionsArgument = {
|
|
|
34
39
|
} as const;
|
|
35
40
|
|
|
36
41
|
export const sqlFamilyAuthoringFieldPresets = {
|
|
37
|
-
|
|
42
|
+
uuidString: {
|
|
38
43
|
kind: 'fieldPreset',
|
|
39
44
|
output: {
|
|
40
45
|
codecId: CHARACTER_CODEC_ID,
|
|
@@ -91,7 +96,7 @@ export const sqlFamilyAuthoringFieldPresets = {
|
|
|
91
96
|
},
|
|
92
97
|
},
|
|
93
98
|
id: {
|
|
94
|
-
|
|
99
|
+
uuidv4String: {
|
|
95
100
|
kind: 'fieldPreset',
|
|
96
101
|
output: {
|
|
97
102
|
codecId: CHARACTER_CODEC_ID,
|
|
@@ -108,7 +113,7 @@ export const sqlFamilyAuthoringFieldPresets = {
|
|
|
108
113
|
id: true,
|
|
109
114
|
},
|
|
110
115
|
},
|
|
111
|
-
|
|
116
|
+
uuidv7String: {
|
|
112
117
|
kind: 'fieldPreset',
|
|
113
118
|
output: {
|
|
114
119
|
codecId: CHARACTER_CODEC_ID,
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Contract,
|
|
3
|
-
ContractMarkerRecord,
|
|
4
|
-
LedgerEntryRecord,
|
|
5
|
-
} from '@prisma-next/contract/types';
|
|
1
|
+
import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
|
|
6
2
|
import type {
|
|
7
3
|
ControlAdapterInstance,
|
|
8
4
|
ControlStack,
|
|
9
5
|
} from '@prisma-next/framework-components/control';
|
|
10
|
-
import type {
|
|
11
|
-
PostgresEnumStorageEntry,
|
|
12
|
-
SqlControlDriverInstance,
|
|
13
|
-
SqlStorage,
|
|
14
|
-
} from '@prisma-next/sql-contract/types';
|
|
6
|
+
import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
|
|
15
7
|
import type {
|
|
16
8
|
AnyQueryAst,
|
|
17
9
|
DdlNode,
|
|
18
10
|
LoweredStatement,
|
|
19
11
|
LowererContext,
|
|
12
|
+
SqlExecuteRequest,
|
|
20
13
|
} from '@prisma-next/sql-relational-core/ast';
|
|
21
14
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
22
15
|
import type { DefaultNormalizer, NativeTypeNormalizer } from './schema-verify/verify-sql-schema';
|
|
@@ -32,6 +25,19 @@ export interface Lowerer {
|
|
|
32
25
|
lower(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
|
|
33
26
|
}
|
|
34
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Extends {@link Lowerer} with async codec-routed DDL lowering. Control
|
|
30
|
+
* adapters implement this; the planner's `CreateTableCall.toOp` and
|
|
31
|
+
* `CreateSchemaCall.toOp` accept it to produce executable DDL statements
|
|
32
|
+
* with literal defaults encoded through their codec.
|
|
33
|
+
*/
|
|
34
|
+
export interface ExecuteRequestLowerer extends Lowerer {
|
|
35
|
+
lowerToExecuteRequest(
|
|
36
|
+
ast: AnyQueryAst | DdlNode,
|
|
37
|
+
context?: LowererContext<unknown>,
|
|
38
|
+
): Promise<SqlExecuteRequest>;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* SQL control adapter interface for control-plane operations.
|
|
37
43
|
* Implemented by target-specific adapters (e.g., Postgres, MySQL).
|
|
@@ -39,7 +45,8 @@ export interface Lowerer {
|
|
|
39
45
|
* @template TTarget - The target ID (e.g., 'postgres', 'mysql')
|
|
40
46
|
*/
|
|
41
47
|
export interface SqlControlAdapter<TTarget extends string = string>
|
|
42
|
-
extends ControlAdapterInstance<'sql', TTarget
|
|
48
|
+
extends ControlAdapterInstance<'sql', TTarget>,
|
|
49
|
+
ExecuteRequestLowerer {
|
|
43
50
|
/**
|
|
44
51
|
* Reads the contract marker for `space` from the database, returning
|
|
45
52
|
* `null` if no marker row exists for that space (or if the marker
|
|
@@ -181,33 +188,6 @@ export interface SqlControlAdapter<TTarget extends string = string>
|
|
|
181
188
|
*/
|
|
182
189
|
readonly normalizeNativeType?: NativeTypeNormalizer;
|
|
183
190
|
|
|
184
|
-
/**
|
|
185
|
-
* Optional bridging adapter for resolving the existing values of a
|
|
186
|
-
* native enum type from the introspected schema IR. Targets supply
|
|
187
|
-
* this so the family-level schema verifier can walk
|
|
188
|
-
* `PostgresEnumStorageEntry` entries natively without needing to
|
|
189
|
-
* know the target-specific `schema.annotations` shape
|
|
190
|
-
* (e.g. `schema.annotations.pg.storageTypes`).
|
|
191
|
-
*/
|
|
192
|
-
readonly resolveExistingEnumValues?: (
|
|
193
|
-
schema: SqlSchemaIR,
|
|
194
|
-
enumType: PostgresEnumStorageEntry,
|
|
195
|
-
namespaceId: string,
|
|
196
|
-
) => readonly string[] | null;
|
|
197
|
-
/**
|
|
198
|
-
* Optional contract-scoped factory for {@link resolveExistingEnumValues}.
|
|
199
|
-
* Targets that need the contract storage to resolve namespace → DDL schema
|
|
200
|
-
* supply this; the family control instance prefers it over the bare adapter
|
|
201
|
-
* hook when present.
|
|
202
|
-
*/
|
|
203
|
-
readonly resolveExistingEnumValuesForContract?: (
|
|
204
|
-
contract: Contract<SqlStorage>,
|
|
205
|
-
) => (
|
|
206
|
-
schema: SqlSchemaIR,
|
|
207
|
-
enumType: PostgresEnumStorageEntry,
|
|
208
|
-
namespaceId: string,
|
|
209
|
-
) => readonly string[] | null;
|
|
210
|
-
|
|
211
191
|
/**
|
|
212
192
|
* Ordered DDL queries that bootstrap marker/ledger control tables for migration
|
|
213
193
|
* runners. Postgres includes `CREATE SCHEMA`; SQLite does not.
|
|
@@ -219,17 +199,6 @@ export interface SqlControlAdapter<TTarget extends string = string>
|
|
|
219
199
|
* `sign` — excludes the ledger table.
|
|
220
200
|
*/
|
|
221
201
|
bootstrapSignMarkerQueries(): readonly DdlNode[];
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Lower a SQL query AST into a target-flavored `{ sql, params }` payload.
|
|
225
|
-
*
|
|
226
|
-
* Migration tooling (e.g. the `dataTransform` operation) needs to materialize
|
|
227
|
-
* SQL at emit/plan time without instantiating the runtime adapter. The control
|
|
228
|
-
* adapter's `lower` is byte-equivalent to the runtime adapter's `lower` for the
|
|
229
|
-
* same AST and contract, ensuring planned SQL matches what the runtime would
|
|
230
|
-
* emit.
|
|
231
|
-
*/
|
|
232
|
-
lower(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
|
|
233
202
|
}
|
|
234
203
|
|
|
235
204
|
/**
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from '@prisma-next/framework-components/control';
|
|
5
5
|
import type { EmissionSpi } from '@prisma-next/framework-components/emission';
|
|
6
6
|
import { sqlEmission } from '@prisma-next/sql-contract-emitter';
|
|
7
|
+
import { sqlFamilyEntityTypes, sqlFamilyPslBlockDescriptors } from './authoring-entity-types';
|
|
7
8
|
import { sqlFamilyAuthoringFieldPresets } from './authoring-field-presets';
|
|
8
9
|
import { sqlFamilyAuthoringTypes } from './authoring-type-constructors';
|
|
9
10
|
import { createSqlFamilyInstance, type SqlControlFamilyInstance } from './control-instance';
|
|
@@ -19,6 +20,8 @@ export class SqlFamilyDescriptor
|
|
|
19
20
|
readonly authoring = {
|
|
20
21
|
field: sqlFamilyAuthoringFieldPresets,
|
|
21
22
|
type: sqlFamilyAuthoringTypes,
|
|
23
|
+
entityTypes: sqlFamilyEntityTypes,
|
|
24
|
+
pslBlockDescriptors: sqlFamilyPslBlockDescriptors,
|
|
22
25
|
} as const;
|
|
23
26
|
|
|
24
27
|
create<TTargetId extends string>(
|
|
@@ -35,8 +35,8 @@ import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-cont
|
|
|
35
35
|
import type {
|
|
36
36
|
AnyQueryAst,
|
|
37
37
|
DdlNode,
|
|
38
|
-
LoweredStatement,
|
|
39
38
|
LowererContext,
|
|
39
|
+
SqlExecuteRequest,
|
|
40
40
|
} from '@prisma-next/sql-relational-core/ast';
|
|
41
41
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
42
42
|
import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
@@ -68,10 +68,10 @@ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
|
68
68
|
) {
|
|
69
69
|
const namespaces = contract.storage.namespaces as Record<
|
|
70
70
|
string,
|
|
71
|
-
{ readonly entries:
|
|
71
|
+
{ readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> }
|
|
72
72
|
>;
|
|
73
73
|
for (const ns of Object.values(namespaces)) {
|
|
74
|
-
const tbls = ns.entries
|
|
74
|
+
const tbls = ns.entries['table'];
|
|
75
75
|
if (typeof tbls !== 'object' || tbls === null) continue;
|
|
76
76
|
for (const table of Object.values(tbls)) {
|
|
77
77
|
if (
|
|
@@ -240,7 +240,10 @@ export interface SqlControlFamilyInstance
|
|
|
240
240
|
|
|
241
241
|
inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
|
|
242
242
|
|
|
243
|
-
lowerAst(
|
|
243
|
+
lowerAst(
|
|
244
|
+
ast: AnyQueryAst | DdlNode,
|
|
245
|
+
context: LowererContext<unknown>,
|
|
246
|
+
): Promise<SqlExecuteRequest>;
|
|
244
247
|
|
|
245
248
|
/**
|
|
246
249
|
* Inserts the initial marker row for `space` (upsert on `space`).
|
|
@@ -672,9 +675,6 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
672
675
|
}): VerifyDatabaseSchemaResult {
|
|
673
676
|
const contract = deserializeWithTargetSerializer(options.contract) as Contract<SqlStorage>;
|
|
674
677
|
const controlAdapter = getControlAdapter();
|
|
675
|
-
const resolveExistingEnumValues =
|
|
676
|
-
controlAdapter.resolveExistingEnumValuesForContract?.(contract) ??
|
|
677
|
-
controlAdapter.resolveExistingEnumValues;
|
|
678
678
|
return verifySqlSchema({
|
|
679
679
|
contract,
|
|
680
680
|
schema: options.schema,
|
|
@@ -683,7 +683,6 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
683
683
|
frameworkComponents: options.frameworkComponents,
|
|
684
684
|
...ifDefined('normalizeDefault', controlAdapter.normalizeDefault),
|
|
685
685
|
...ifDefined('normalizeNativeType', controlAdapter.normalizeNativeType),
|
|
686
|
-
...ifDefined('resolveExistingEnumValues', resolveExistingEnumValues),
|
|
687
686
|
});
|
|
688
687
|
},
|
|
689
688
|
async sign(options: {
|
|
@@ -707,7 +706,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
707
706
|
const controlAdapter = getControlAdapter();
|
|
708
707
|
const lowererContext = { contract };
|
|
709
708
|
for (const query of controlAdapter.bootstrapSignMarkerQueries()) {
|
|
710
|
-
const lowered = controlAdapter.
|
|
709
|
+
const lowered = await controlAdapter.lowerToExecuteRequest(query, lowererContext);
|
|
711
710
|
await driver.query(lowered.sql, lowered.params);
|
|
712
711
|
}
|
|
713
712
|
|
|
@@ -857,8 +856,11 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
857
856
|
return sqlSchemaIrToPslAst(schemaIR);
|
|
858
857
|
},
|
|
859
858
|
|
|
860
|
-
lowerAst(
|
|
861
|
-
|
|
859
|
+
lowerAst(
|
|
860
|
+
ast: AnyQueryAst | DdlNode,
|
|
861
|
+
context: LowererContext<unknown>,
|
|
862
|
+
): Promise<SqlExecuteRequest> {
|
|
863
|
+
return getControlAdapter().lowerToExecuteRequest(ast, context);
|
|
862
864
|
},
|
|
863
865
|
|
|
864
866
|
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '@prisma-next/sql-contract/types';
|
|
22
22
|
import {
|
|
23
23
|
createSqlContractSchema,
|
|
24
|
+
createSqlEntrySchemaRegistry,
|
|
24
25
|
validateSqlContractFully,
|
|
25
26
|
} from '@prisma-next/sql-contract/validators';
|
|
26
27
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
@@ -72,18 +73,20 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
72
73
|
private readonly contractSchema: Type<unknown> | undefined;
|
|
73
74
|
|
|
74
75
|
constructor(
|
|
75
|
-
protected readonly
|
|
76
|
+
protected readonly entityHydrationRegistry: ReadonlyMap<
|
|
76
77
|
string,
|
|
77
78
|
SqlEntityHydrationFactory
|
|
78
79
|
> = new Map(),
|
|
79
|
-
|
|
80
|
+
validatorRegistry: ReadonlyMap<string, Type<unknown>> = new Map(),
|
|
80
81
|
) {
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
82
|
+
// One uniform registry: SQL core's kinds and pack contributions are
|
|
83
|
+
// composed into the same map. Only build a per-instance contract
|
|
84
|
+
// schema when pack contributions exist — the cached module-level
|
|
85
|
+
// default in `validators.ts` is the same composition with no pack
|
|
86
|
+
// entries and avoids per-instance schema compilation.
|
|
84
87
|
this.contractSchema =
|
|
85
|
-
|
|
86
|
-
? createSqlContractSchema(
|
|
88
|
+
validatorRegistry.size > 0
|
|
89
|
+
? createSqlContractSchema(createSqlEntrySchemaRegistry(validatorRegistry))
|
|
87
90
|
: undefined;
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -182,74 +185,87 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
182
185
|
return raw;
|
|
183
186
|
}
|
|
184
187
|
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
185
|
-
if (
|
|
186
|
-
Object.hasOwn(rawRecord, 'tables') ||
|
|
187
|
-
Object.hasOwn(rawRecord, 'enum') ||
|
|
188
|
-
Object.hasOwn(rawRecord, 'collections')
|
|
189
|
-
) {
|
|
190
|
-
throw new ContractValidationError(
|
|
191
|
-
'Namespace envelope uses deprecated flat slot keys; expected `entries: { table? }`',
|
|
192
|
-
'structural',
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
188
|
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
196
189
|
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
197
190
|
if (parsed instanceof type.errors) {
|
|
198
191
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
199
192
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
200
193
|
}
|
|
201
|
-
|
|
202
|
-
const entriesInput: {
|
|
203
|
-
table: Record<string, StorageTable>;
|
|
204
|
-
valueSet?: Record<string, StorageValueSet>;
|
|
205
|
-
} = { table: {} };
|
|
194
|
+
const entriesOutput: Record<string, Record<string, unknown>> = {};
|
|
206
195
|
const entriesRaw = parsed.entries;
|
|
207
196
|
if (entriesRaw !== undefined && typeof entriesRaw === 'object' && entriesRaw !== null) {
|
|
208
197
|
const rawEntries = entriesRaw as Record<string, unknown>;
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
198
|
+
for (const [key, innerMap] of Object.entries(rawEntries)) {
|
|
199
|
+
const hydrated = this.hydrateEntriesKind(key, innerMap);
|
|
200
|
+
if (hydrated === undefined) {
|
|
201
|
+
throw new ContractValidationError(
|
|
202
|
+
`Unknown entries key "${key}" in namespace "${id}"; no hydration factory registered for this entity kind`,
|
|
203
|
+
'structural',
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
entriesOutput[key] = hydrated;
|
|
217
207
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
!Array.isArray(valueSetSlot)
|
|
223
|
-
) {
|
|
224
|
-
entriesInput.valueSet = Object.fromEntries(
|
|
225
|
-
Object.entries(
|
|
226
|
-
blindCast<
|
|
227
|
-
Record<string, unknown>,
|
|
228
|
-
'valueSet slot is a plain record after object check'
|
|
229
|
-
>(valueSetSlot),
|
|
230
|
-
).map(([vsName, vs]) => [
|
|
231
|
-
vsName,
|
|
232
|
-
vs instanceof StorageValueSet
|
|
233
|
-
? vs
|
|
234
|
-
: new StorageValueSet(
|
|
235
|
-
blindCast<
|
|
236
|
-
StorageValueSetInput,
|
|
237
|
-
'non-instance valueSet entry is StorageValueSetInput'
|
|
238
|
-
>(vs),
|
|
239
|
-
),
|
|
240
|
-
]),
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
// Target-specific slots (e.g. postgres `type`) are left for target
|
|
244
|
-
// overrides to extract from the original `raw` parameter.
|
|
208
|
+
}
|
|
209
|
+
// Always ensure a 'table' key is present (may be empty).
|
|
210
|
+
if (!Object.hasOwn(entriesOutput, 'table')) {
|
|
211
|
+
entriesOutput['table'] = {};
|
|
245
212
|
}
|
|
246
213
|
|
|
247
|
-
return blindCast<SqlNamespaceTablesInput, 'hydrated namespace
|
|
214
|
+
return blindCast<SqlNamespaceTablesInput, 'hydrated namespace entries input'>({
|
|
248
215
|
id,
|
|
249
|
-
entries:
|
|
216
|
+
entries: entriesOutput,
|
|
250
217
|
});
|
|
251
218
|
}
|
|
252
219
|
|
|
220
|
+
protected hydrateEntriesKind(
|
|
221
|
+
key: string,
|
|
222
|
+
innerMap: unknown,
|
|
223
|
+
): Record<string, unknown> | undefined {
|
|
224
|
+
if (key === 'table') {
|
|
225
|
+
if (innerMap === null || typeof innerMap !== 'object' || Array.isArray(innerMap)) {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
return Object.fromEntries(
|
|
229
|
+
Object.entries(
|
|
230
|
+
blindCast<
|
|
231
|
+
Record<string, unknown>,
|
|
232
|
+
'table inner map is a plain record after object check'
|
|
233
|
+
>(innerMap),
|
|
234
|
+
).map(([tableName, table]) => [
|
|
235
|
+
tableName,
|
|
236
|
+
new StorageTable(
|
|
237
|
+
blindCast<
|
|
238
|
+
StorageTableInput,
|
|
239
|
+
'each table value is StorageTableInput by contract schema'
|
|
240
|
+
>(table),
|
|
241
|
+
),
|
|
242
|
+
]),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (key === 'valueSet') {
|
|
246
|
+
if (innerMap === null || typeof innerMap !== 'object' || Array.isArray(innerMap)) {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
return Object.fromEntries(
|
|
250
|
+
Object.entries(
|
|
251
|
+
blindCast<
|
|
252
|
+
Record<string, unknown>,
|
|
253
|
+
'valueSet inner map is a plain record after object check'
|
|
254
|
+
>(innerMap),
|
|
255
|
+
).map(([vsName, vs]) => [
|
|
256
|
+
vsName,
|
|
257
|
+
new StorageValueSet(
|
|
258
|
+
blindCast<StorageValueSetInput, 'valueSet entry is StorageValueSetInput after parse'>(
|
|
259
|
+
vs,
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
]),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
// Delegate unknown keys to subclass — return undefined to fail closed.
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
253
269
|
protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {
|
|
254
270
|
if (typeof entry !== 'object' || entry === null) {
|
|
255
271
|
return entry;
|
|
@@ -258,7 +274,7 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
258
274
|
if (typeof kind !== 'string') {
|
|
259
275
|
return entry;
|
|
260
276
|
}
|
|
261
|
-
const factory = this.
|
|
277
|
+
const factory = this.entityHydrationRegistry.get(kind);
|
|
262
278
|
if (factory === undefined) {
|
|
263
279
|
return entry;
|
|
264
280
|
}
|