@prisma-next/family-sql 0.13.0-dev.4 → 0.13.0-dev.40
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 +6 -4
- 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-BR2vC7Z-.mjs} +27 -27
- package/dist/sql-contract-serializer-BR2vC7Z-.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 +44 -75
- package/src/core/ir/sql-contract-serializer.ts +7 -0
- 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[] {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
|
|
2
|
+
import { isPlainRecord } from '@prisma-next/contract/is-plain-record';
|
|
2
3
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
4
|
import type { ContractSerializer } from '@prisma-next/framework-components/control';
|
|
4
5
|
import {
|
|
6
|
+
type AnyEntityKindDescriptor,
|
|
7
|
+
hydrateNamespaceEntities,
|
|
5
8
|
type Namespace,
|
|
6
9
|
NamespaceBase,
|
|
7
10
|
UNBOUND_NAMESPACE_ID,
|
|
8
11
|
} from '@prisma-next/framework-components/ir';
|
|
9
12
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
13
|
+
import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
|
|
10
14
|
import {
|
|
11
15
|
buildSqlNamespace,
|
|
12
16
|
type SqlNamespaceTablesInput,
|
|
@@ -14,10 +18,6 @@ import {
|
|
|
14
18
|
type SqlStorageInput,
|
|
15
19
|
type SqlStorageTypeEntry,
|
|
16
20
|
SqlUnboundNamespace,
|
|
17
|
-
StorageTable,
|
|
18
|
-
type StorageTableInput,
|
|
19
|
-
StorageValueSet,
|
|
20
|
-
type StorageValueSetInput,
|
|
21
21
|
} from '@prisma-next/sql-contract/types';
|
|
22
22
|
import {
|
|
23
23
|
createSqlContractSchema,
|
|
@@ -36,10 +36,6 @@ const NamespaceRawSchema = type({
|
|
|
36
36
|
}),
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
export type SqlEntityHydrationFactory = (entry: unknown) => unknown;
|
|
44
40
|
|
|
45
41
|
/**
|
|
@@ -70,21 +66,18 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
70
66
|
implements ContractSerializer<TContract>
|
|
71
67
|
{
|
|
72
68
|
private readonly contractSchema: Type<unknown> | undefined;
|
|
69
|
+
private readonly entryKinds: ReadonlyMap<string, AnyEntityKindDescriptor>;
|
|
73
70
|
|
|
74
71
|
constructor(
|
|
75
|
-
protected readonly
|
|
72
|
+
protected readonly entityHydrationRegistry: ReadonlyMap<
|
|
76
73
|
string,
|
|
77
74
|
SqlEntityHydrationFactory
|
|
78
75
|
> = new Map(),
|
|
79
|
-
|
|
76
|
+
packEntityKinds: readonly AnyEntityKindDescriptor[] = [],
|
|
80
77
|
) {
|
|
81
|
-
|
|
82
|
-
// exist. The cached module-level default in `validators.ts` covers the
|
|
83
|
-
// no-contributions case and avoids per-instance schema compilation.
|
|
78
|
+
this.entryKinds = composeSqlEntityKinds(packEntityKinds);
|
|
84
79
|
this.contractSchema =
|
|
85
|
-
|
|
86
|
-
? createSqlContractSchema(validatorFragments)
|
|
87
|
-
: undefined;
|
|
80
|
+
packEntityKinds.length > 0 ? createSqlContractSchema(this.entryKinds) : undefined;
|
|
88
81
|
}
|
|
89
82
|
|
|
90
83
|
deserializeContract<T extends TContract = TContract>(json: unknown): T {
|
|
@@ -136,7 +129,19 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
136
129
|
// deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
|
|
137
130
|
// write into. Once runtime-qualification routes table lookups by namespace, this
|
|
138
131
|
// shim should be removed.
|
|
139
|
-
|
|
132
|
+
//
|
|
133
|
+
// TML-2916: the shim only fires when the target's default namespace IS unbound
|
|
134
|
+
// (SQLite, Mongo). On Postgres (`defaultNamespaceId === 'public'`) injecting an
|
|
135
|
+
// empty `__unbound__` slot violates ADR 223 — un-namespaced PG models belong in
|
|
136
|
+
// `public`, not `__unbound__`.
|
|
137
|
+
const withInjectedUnbound =
|
|
138
|
+
this.defaultNamespaceId === UNBOUND_NAMESPACE_ID
|
|
139
|
+
? {
|
|
140
|
+
...hydratedNamespaces,
|
|
141
|
+
[UNBOUND_NAMESPACE_ID]:
|
|
142
|
+
hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance,
|
|
143
|
+
}
|
|
144
|
+
: hydratedNamespaces;
|
|
140
145
|
|
|
141
146
|
return {
|
|
142
147
|
...validated,
|
|
@@ -147,12 +152,14 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
147
152
|
// framework `Namespace` to the SQL-family `SqlNamespace`.
|
|
148
153
|
namespaces: blindCast<
|
|
149
154
|
SqlStorageInput['namespaces'],
|
|
150
|
-
'
|
|
151
|
-
>(
|
|
155
|
+
'hydrateSqlNamespaceMap builds each namespace through the SQL family concretions (SqlBoundNamespace / target schema), so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
|
|
156
|
+
>(withInjectedUnbound),
|
|
152
157
|
}),
|
|
153
158
|
};
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
protected abstract get defaultNamespaceId(): string;
|
|
162
|
+
|
|
156
163
|
protected hydrateSqlNamespaceMap(
|
|
157
164
|
namespaces: Readonly<Record<string, Namespace | Record<string, unknown>>>,
|
|
158
165
|
): Readonly<Record<string, Namespace>> {
|
|
@@ -182,71 +189,33 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
182
189
|
return raw;
|
|
183
190
|
}
|
|
184
191
|
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
192
|
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
196
193
|
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
197
194
|
if (parsed instanceof type.errors) {
|
|
198
195
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
199
196
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
200
197
|
}
|
|
201
|
-
// Default to empty table; overwritten below if raw entries carry a table slot.
|
|
202
|
-
const entriesInput: {
|
|
203
|
-
table: Record<string, StorageTable>;
|
|
204
|
-
valueSet?: Record<string, StorageValueSet>;
|
|
205
|
-
} = { table: {} };
|
|
206
198
|
const entriesRaw = parsed.entries;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
valueSetSlot !== null &&
|
|
221
|
-
typeof valueSetSlot === 'object' &&
|
|
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.
|
|
199
|
+
const rawEntriesMap = isPlainRecord(entriesRaw) ? entriesRaw : {};
|
|
200
|
+
|
|
201
|
+
const entriesInput: Record<string, Readonly<Record<string, unknown>>> = {};
|
|
202
|
+
for (const [key, innerMap] of Object.entries(rawEntriesMap)) {
|
|
203
|
+
entriesInput[key] = isPlainRecord(innerMap) ? innerMap : Object.freeze({});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const entriesOutput = hydrateNamespaceEntities(entriesInput, this.entryKinds, 'fail', id);
|
|
207
|
+
|
|
208
|
+
// Always ensure a 'table' key is present (may be empty).
|
|
209
|
+
if (!Object.hasOwn(entriesOutput, 'table')) {
|
|
210
|
+
entriesOutput['table'] = {};
|
|
245
211
|
}
|
|
246
212
|
|
|
247
|
-
return blindCast<
|
|
213
|
+
return blindCast<
|
|
214
|
+
SqlNamespaceTablesInput,
|
|
215
|
+
'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceTablesInput the family createNamespace consumes.'
|
|
216
|
+
>({
|
|
248
217
|
id,
|
|
249
|
-
entries:
|
|
218
|
+
entries: entriesOutput,
|
|
250
219
|
});
|
|
251
220
|
}
|
|
252
221
|
|
|
@@ -258,7 +227,7 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
258
227
|
if (typeof kind !== 'string') {
|
|
259
228
|
return entry;
|
|
260
229
|
}
|
|
261
|
-
const factory = this.
|
|
230
|
+
const factory = this.entityHydrationRegistry.get(kind);
|
|
262
231
|
if (factory === undefined) {
|
|
263
232
|
return entry;
|
|
264
233
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
2
3
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
4
|
import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
4
5
|
|
|
@@ -15,4 +16,10 @@ export class SqlContractSerializer extends SqlContractSerializerBase<Contract<Sq
|
|
|
15
16
|
constructor() {
|
|
16
17
|
super(new Map());
|
|
17
18
|
}
|
|
19
|
+
|
|
20
|
+
// Family-level fallback when no target descriptor is wired in. Preserves the
|
|
21
|
+
// pre-TML-2916 compatibility-shim behaviour for the unbound slot.
|
|
22
|
+
protected override get defaultNamespaceId(): string {
|
|
23
|
+
return UNBOUND_NAMESPACE_ID;
|
|
24
|
+
}
|
|
18
25
|
}
|