@prisma-next/family-sql 0.14.0-dev.5 → 0.14.0-dev.51
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 → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
- package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
- package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BVOj4gXI.d.mts} +42 -8
- package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +238 -6
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +336 -942
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +150 -0
- package/dist/diff.d.mts.map +1 -0
- package/dist/diff.mjs +188 -0
- package/dist/diff.mjs.map +1 -0
- package/dist/ir.d.mts +26 -9
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -2
- package/dist/ir.mjs.map +1 -1
- package/dist/migration.d.mts +11 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +7 -0
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/psl-infer.d.mts +101 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +342 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/schema-differ-DnoopSXm.d.mts +45 -0
- package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
- package/dist/schema-verify-W3r631Jh.mjs +226 -0
- package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
- package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
- package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
- package/dist/{types-kgstZ_Zd.d.mts → types-BW7pzb2j.d.mts} +34 -139
- package/dist/types-BW7pzb2j.d.mts.map +1 -0
- package/package.json +23 -23
- package/src/core/authoring-entity-types.ts +12 -37
- package/src/core/control-adapter.ts +10 -5
- package/src/core/control-instance.ts +210 -42
- package/src/core/control-target-descriptor.ts +98 -0
- package/src/core/diff/diff-tree-normalization.ts +211 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +77 -0
- package/src/core/diff/verifier-disposition.ts +30 -0
- package/src/core/ir/sql-contract-serializer-base.ts +70 -56
- package/src/core/ir/sql-contract-serializer.ts +5 -7
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +110 -30
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +41 -0
- package/src/core/migrations/types.ts +31 -34
- package/src/core/psl-contract-infer/printer-config.ts +12 -6
- package/src/core/psl-contract-infer/relation-inference.ts +9 -1
- package/src/core/sql-migration.ts +12 -1
- package/src/exports/control.ts +11 -1
- package/src/exports/diff.ts +38 -0
- package/src/exports/psl-infer.ts +39 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
- package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
- package/dist/schema-verify.d.mts +0 -39
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
- package/dist/test-utils.d.mts +0 -2
- package/dist/test-utils.mjs +0 -2
- package/dist/types-kgstZ_Zd.d.mts.map +0 -1
- package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
- package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
- package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
- package/src/core/schema-verify/control-verify-emit.ts +0 -46
- package/src/core/schema-verify/verifier-disposition.ts +0 -58
- package/src/core/schema-verify/verify-helpers.ts +0 -820
- package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
- package/src/exports/schema-verify.ts +0 -18
- package/src/exports/test-utils.ts +0 -9
|
@@ -6,18 +6,15 @@ import {
|
|
|
6
6
|
type AnyEntityKindDescriptor,
|
|
7
7
|
hydrateNamespaceEntities,
|
|
8
8
|
type Namespace,
|
|
9
|
-
NamespaceBase,
|
|
10
|
-
UNBOUND_NAMESPACE_ID,
|
|
11
9
|
} from '@prisma-next/framework-components/ir';
|
|
12
10
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
13
11
|
import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
|
|
14
12
|
import {
|
|
15
|
-
|
|
16
|
-
type
|
|
13
|
+
isMaterializedSqlNamespace,
|
|
14
|
+
type SqlNamespaceInput,
|
|
17
15
|
SqlStorage,
|
|
18
16
|
type SqlStorageInput,
|
|
19
17
|
type SqlStorageTypeEntry,
|
|
20
|
-
SqlUnboundNamespace,
|
|
21
18
|
} from '@prisma-next/sql-contract/types';
|
|
22
19
|
import {
|
|
23
20
|
createSqlContractSchema,
|
|
@@ -25,7 +22,7 @@ import {
|
|
|
25
22
|
} from '@prisma-next/sql-contract/validators';
|
|
26
23
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
27
24
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
28
|
-
import type { JsonObject } from '@prisma-next/utils/json';
|
|
25
|
+
import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
|
|
29
26
|
import { type Type, type } from 'arktype';
|
|
30
27
|
|
|
31
28
|
const NamespaceRawSchema = type({
|
|
@@ -120,77 +117,48 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
120
117
|
'structural',
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
|
-
const hydratedNamespaces = this.hydrateSqlNamespaceMap(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
|
|
130
|
-
// write into. Once runtime-qualification routes table lookups by namespace, this
|
|
131
|
-
// shim should be removed.
|
|
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;
|
|
120
|
+
const hydratedNamespaces = this.hydrateSqlNamespaceMap(
|
|
121
|
+
blindCast<
|
|
122
|
+
Readonly<Record<string, Record<string, unknown>>>,
|
|
123
|
+
'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'
|
|
124
|
+
>(rawNamespaces),
|
|
125
|
+
);
|
|
145
126
|
|
|
146
127
|
return {
|
|
147
128
|
...validated,
|
|
148
129
|
storage: new SqlStorage({
|
|
149
130
|
storageHash: validated.storage.storageHash,
|
|
150
131
|
...ifDefined('types', hydratedTypes),
|
|
151
|
-
// Cast narrows the result of hydrateSqlNamespaceMap from the wider
|
|
152
|
-
// framework `Namespace` to the SQL-family `SqlNamespace`.
|
|
153
132
|
namespaces: blindCast<
|
|
154
133
|
SqlStorageInput['namespaces'],
|
|
155
|
-
'hydrateSqlNamespaceMap builds each namespace through the
|
|
156
|
-
>(
|
|
134
|
+
'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
|
|
135
|
+
>(hydratedNamespaces),
|
|
157
136
|
}),
|
|
158
137
|
};
|
|
159
138
|
}
|
|
160
139
|
|
|
161
|
-
protected abstract get defaultNamespaceId(): string;
|
|
162
|
-
|
|
163
140
|
protected hydrateSqlNamespaceMap(
|
|
164
|
-
namespaces: Readonly<Record<string,
|
|
141
|
+
namespaces: Readonly<Record<string, Record<string, unknown>>>,
|
|
165
142
|
): Readonly<Record<string, Namespace>> {
|
|
166
143
|
return Object.fromEntries(
|
|
167
144
|
Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
|
|
168
|
-
// Raw entries passed structural validation; hydrate materialises family IR class instances.
|
|
169
145
|
const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
|
|
177
|
-
>(namespaceHydrated),
|
|
178
|
-
);
|
|
179
|
-
return [nsId, namespaceMaterialised];
|
|
146
|
+
if (!isMaterializedSqlNamespace(namespaceHydrated)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return [nsId, namespaceHydrated];
|
|
180
152
|
}),
|
|
181
153
|
);
|
|
182
154
|
}
|
|
183
155
|
|
|
184
156
|
protected hydrateSqlNamespaceEntry(
|
|
185
157
|
nsId: string,
|
|
186
|
-
raw:
|
|
187
|
-
): Namespace |
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
192
|
-
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
193
|
-
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
158
|
+
raw: Record<string, unknown>,
|
|
159
|
+
): Namespace | SqlNamespaceInput {
|
|
160
|
+
const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;
|
|
161
|
+
const parsed = NamespaceRawSchema({ ...raw, id });
|
|
194
162
|
if (parsed instanceof type.errors) {
|
|
195
163
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
196
164
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
@@ -211,8 +179,8 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
211
179
|
}
|
|
212
180
|
|
|
213
181
|
return blindCast<
|
|
214
|
-
|
|
215
|
-
'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the
|
|
182
|
+
SqlNamespaceInput,
|
|
183
|
+
'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'
|
|
216
184
|
>({
|
|
217
185
|
id,
|
|
218
186
|
entries: entriesOutput,
|
|
@@ -240,4 +208,50 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
240
208
|
protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
|
|
241
209
|
return hydrated as TContract;
|
|
242
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Serializes a namespace's `entries` dict by walking every enumerable
|
|
214
|
+
* kind — no kind is named here, mirroring the generic hydrate walk in
|
|
215
|
+
* `hydrateSqlNamespaceEntry` above. `table` is the SQL family's one
|
|
216
|
+
* universal base kind (every namespace carries it), so it is always
|
|
217
|
+
* emitted, even when empty; every other kind — target- or
|
|
218
|
+
* pack-contributed — is emitted only when it holds at least one entry.
|
|
219
|
+
* A kind carried non-enumerable on `entries` is excluded here for free,
|
|
220
|
+
* since `Object.entries` honors enumerability.
|
|
221
|
+
*/
|
|
222
|
+
protected serializeNamespaceEntries(
|
|
223
|
+
entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
|
|
224
|
+
): Record<string, Record<string, JsonObject>> {
|
|
225
|
+
const out: Record<string, Record<string, JsonObject>> = {
|
|
226
|
+
table: this.serializeEntries(entries['table'] ?? {}),
|
|
227
|
+
};
|
|
228
|
+
for (const [kind, record] of Object.entries(entries)) {
|
|
229
|
+
if (kind === 'table' || record == null || Object.keys(record).length === 0) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
out[kind] = this.serializeEntries(record);
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private serializeEntries(entries: Readonly<Record<string, unknown>>): Record<string, JsonObject> {
|
|
238
|
+
const out: Record<string, JsonObject> = {};
|
|
239
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
240
|
+
out[name] = this.serializeJsonObject(entry);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
protected serializeJsonObject(value: unknown): JsonObject {
|
|
246
|
+
return blindCast<
|
|
247
|
+
JsonObject,
|
|
248
|
+
'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'
|
|
249
|
+
>(this.serializeJsonValue(value));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private serializeJsonValue(value: unknown): JsonValue {
|
|
253
|
+
return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(
|
|
254
|
+
JSON.parse(JSON.stringify(value)),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
243
257
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
3
2
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
3
|
import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
5
4
|
|
|
@@ -11,15 +10,14 @@ import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
|
11
10
|
* codec-typed envelope wire a target-specific subclass with a populated
|
|
12
11
|
* registry (see Postgres). Family-level call sites instantiate this
|
|
13
12
|
* default directly when no target serializer is supplied.
|
|
13
|
+
*
|
|
14
|
+
* Because this serializer has no target concretion, deserialization of
|
|
15
|
+
* contracts that include namespace entries from JSON will throw unless
|
|
16
|
+
* the caller provides pre-hydrated `NamespaceBase` instances. Production
|
|
17
|
+
* paths always supply a target-specific serializer.
|
|
14
18
|
*/
|
|
15
19
|
export class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
|
|
16
20
|
constructor() {
|
|
17
21
|
super(new Map());
|
|
18
22
|
}
|
|
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
|
-
}
|
|
25
23
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
SchemaDiffIssue,
|
|
3
3
|
SchemaVerifier,
|
|
4
4
|
SchemaVerifyOptions,
|
|
5
5
|
SchemaVerifyResult,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
* SQL family `SchemaVerifier` abstract base. Centralises the SQL-shared
|
|
10
10
|
* walk (table-by-table + column-by-column matching keyed by
|
|
11
11
|
* `(namespace.id, name)`, FK / unique / index comparisons via the
|
|
12
|
-
*
|
|
12
|
+
* shared satisfaction helpers in `sql-schema-diff.ts`) and exposes a protected hook
|
|
13
13
|
* for target extensions (Postgres functions, RLS policies, future
|
|
14
14
|
* target-only kinds).
|
|
15
15
|
*
|
|
@@ -29,7 +29,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
29
29
|
implements SchemaVerifier<TContract, TSchema>
|
|
30
30
|
{
|
|
31
31
|
verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult {
|
|
32
|
-
const issues:
|
|
32
|
+
const issues: SchemaDiffIssue[] = [];
|
|
33
33
|
issues.push(...this.verifyCommonSqlSchema(options));
|
|
34
34
|
issues.push(...this.verifyTargetExtensions(options));
|
|
35
35
|
return { ok: issues.length === 0, issues };
|
|
@@ -43,7 +43,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
43
43
|
*/
|
|
44
44
|
protected abstract verifyCommonSqlSchema(
|
|
45
45
|
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
46
|
-
): readonly
|
|
46
|
+
): readonly SchemaDiffIssue[];
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Target-specific extensions — e.g. Postgres functions, future RLS
|
|
@@ -52,5 +52,5 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
|
52
52
|
*/
|
|
53
53
|
protected abstract verifyTargetExtensions(
|
|
54
54
|
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
55
|
-
): readonly
|
|
55
|
+
): readonly SchemaDiffIssue[];
|
|
56
56
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ColumnDefault, Contract, JsonValue } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecRef } from '@prisma-next/framework-components/codec';
|
|
2
3
|
import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
|
|
3
4
|
import {
|
|
4
5
|
type CheckConstraint,
|
|
@@ -12,16 +13,17 @@ import {
|
|
|
12
13
|
type UniqueConstraint,
|
|
13
14
|
} from '@prisma-next/sql-contract/types';
|
|
14
15
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
15
|
-
import
|
|
16
|
-
SqlAnnotations,
|
|
17
|
-
SqlCheckConstraintIRInput,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import {
|
|
17
|
+
type SqlAnnotations,
|
|
18
|
+
type SqlCheckConstraintIRInput,
|
|
19
|
+
type SqlColumnIRInput,
|
|
20
|
+
type SqlForeignKeyIRInput,
|
|
21
|
+
type SqlIndexIRInput,
|
|
21
22
|
SqlSchemaIR,
|
|
22
23
|
SqlTableIR,
|
|
23
|
-
|
|
24
|
+
type SqlUniqueIRInput,
|
|
24
25
|
} from '@prisma-next/sql-schema-ir/types';
|
|
26
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
25
27
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -72,7 +74,7 @@ function convertColumn(
|
|
|
72
74
|
storageTypes: ResolvedStorageTypes,
|
|
73
75
|
expandNativeType: NativeTypeExpander | undefined,
|
|
74
76
|
renderDefault: DefaultRenderer | undefined,
|
|
75
|
-
):
|
|
77
|
+
): SqlColumnIRInput {
|
|
76
78
|
// Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
|
|
77
79
|
// `typeParams` to a named `storage.types` entry expand the same way as
|
|
78
80
|
// columns that inline those fields. Without this resolution, a
|
|
@@ -82,13 +84,14 @@ function convertColumn(
|
|
|
82
84
|
// typeRef and produces `"vector(1536)"` — making diffs on the same
|
|
83
85
|
// contract falsely report a `type_mismatch`.
|
|
84
86
|
const resolved = resolveColumnTypeMetadata(column, storageTypes);
|
|
85
|
-
const
|
|
87
|
+
const baseNativeType = expandNativeType
|
|
86
88
|
? expandNativeType({
|
|
87
89
|
nativeType: resolved.nativeType,
|
|
88
90
|
codecId: resolved.codecId,
|
|
89
91
|
...ifDefined('typeParams', resolved.typeParams),
|
|
90
92
|
})
|
|
91
93
|
: resolved.nativeType;
|
|
94
|
+
const nativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
|
|
92
95
|
return {
|
|
93
96
|
name,
|
|
94
97
|
nativeType,
|
|
@@ -97,6 +100,43 @@ function convertColumn(
|
|
|
97
100
|
'default',
|
|
98
101
|
column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
|
|
99
102
|
),
|
|
103
|
+
// Contract-derived columns are resolved by construction: the computed
|
|
104
|
+
// full native type doubles as the resolved value, and the contract's
|
|
105
|
+
// structured default is the resolved default (the introspected side
|
|
106
|
+
// stamps its normalizer's parse of the raw expression).
|
|
107
|
+
resolvedNativeType: nativeType,
|
|
108
|
+
...ifDefined('resolvedDefault', column.default ?? undefined),
|
|
109
|
+
// The column's codec identity, carried the same way the query AST
|
|
110
|
+
// carries `CodecRef` (TML-2456) — the migration planner's op-builders
|
|
111
|
+
// resolve DDL rendering from this at plan time (Decision 5), instead of
|
|
112
|
+
// reading a derivation-precomputed render payload.
|
|
113
|
+
codecRef: buildColumnCodecRef(resolved, column.many),
|
|
114
|
+
codecBaseNativeType: resolved.nativeType,
|
|
115
|
+
...(column.typeRef !== undefined ? { codecNamedType: true } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds the column's `CodecRef` from its resolved (post-`typeRef`) codec
|
|
121
|
+
* identity — the same construction the query AST and the migration DDL
|
|
122
|
+
* renderer already use (TML-2456, TML-2918).
|
|
123
|
+
*/
|
|
124
|
+
function buildColumnCodecRef(
|
|
125
|
+
resolved: Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'>,
|
|
126
|
+
many: boolean | undefined,
|
|
127
|
+
): CodecRef {
|
|
128
|
+
return {
|
|
129
|
+
codecId: resolved.codecId,
|
|
130
|
+
...ifDefined(
|
|
131
|
+
'typeParams',
|
|
132
|
+
resolved.typeParams !== undefined
|
|
133
|
+
? blindCast<
|
|
134
|
+
JsonValue,
|
|
135
|
+
'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed (non-undefined) value lands in CodecRef.typeParams which is JsonValue'
|
|
136
|
+
>(resolved.typeParams)
|
|
137
|
+
: undefined,
|
|
138
|
+
),
|
|
139
|
+
...ifDefined('many', many),
|
|
100
140
|
};
|
|
101
141
|
}
|
|
102
142
|
|
|
@@ -192,22 +232,26 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
|
|
|
192
232
|
};
|
|
193
233
|
}
|
|
194
234
|
|
|
195
|
-
function convertUnique(unique: UniqueConstraint):
|
|
235
|
+
function convertUnique(unique: UniqueConstraint): SqlUniqueIRInput {
|
|
196
236
|
return {
|
|
197
237
|
columns: unique.columns,
|
|
198
238
|
...ifDefined('name', unique.name),
|
|
199
239
|
};
|
|
200
240
|
}
|
|
201
241
|
|
|
202
|
-
function convertIndex(index: Index):
|
|
242
|
+
function convertIndex(index: Index): SqlIndexIRInput {
|
|
203
243
|
return {
|
|
204
244
|
columns: index.columns,
|
|
205
245
|
unique: false,
|
|
206
246
|
...ifDefined('name', index.name),
|
|
247
|
+
// Carried so the derived index node compares type/options against the
|
|
248
|
+
// introspected side (the legacy walk read them from the contract).
|
|
249
|
+
...ifDefined('type', index.type),
|
|
250
|
+
...ifDefined('options', index.options),
|
|
207
251
|
};
|
|
208
252
|
}
|
|
209
253
|
|
|
210
|
-
function convertForeignKey(fk: ForeignKey):
|
|
254
|
+
function convertForeignKey(fk: ForeignKey): SqlForeignKeyIRInput {
|
|
211
255
|
return {
|
|
212
256
|
columns: fk.source.columns,
|
|
213
257
|
referencedTable: fk.target.tableName,
|
|
@@ -227,7 +271,7 @@ function convertTable(
|
|
|
227
271
|
renderDefault: DefaultRenderer | undefined,
|
|
228
272
|
storage: SqlStorage,
|
|
229
273
|
): SqlTableIR {
|
|
230
|
-
const columns: Record<string,
|
|
274
|
+
const columns: Record<string, SqlColumnIRInput> = {};
|
|
231
275
|
for (const [colName, colDef] of Object.entries(table.columns)) {
|
|
232
276
|
columns[colName] = convertColumn(
|
|
233
277
|
colName,
|
|
@@ -243,7 +287,7 @@ function convertTable(
|
|
|
243
287
|
...table.uniques.map((unique) => unique.columns.join(',')),
|
|
244
288
|
...(table.primaryKey ? [table.primaryKey.columns.join(',')] : []),
|
|
245
289
|
]);
|
|
246
|
-
const fkBackingIndexes:
|
|
290
|
+
const fkBackingIndexes: SqlIndexIRInput[] = [];
|
|
247
291
|
for (const fk of table.foreignKeys) {
|
|
248
292
|
if (fk.index === false) continue;
|
|
249
293
|
const key = fk.source.columns.join(',');
|
|
@@ -261,7 +305,7 @@ function convertTable(
|
|
|
261
305
|
? table.checks.map((c) => convertCheck(c, storage))
|
|
262
306
|
: undefined;
|
|
263
307
|
|
|
264
|
-
return {
|
|
308
|
+
return new SqlTableIR({
|
|
265
309
|
name,
|
|
266
310
|
columns,
|
|
267
311
|
...ifDefined('primaryKey', table.primaryKey),
|
|
@@ -269,7 +313,7 @@ function convertTable(
|
|
|
269
313
|
uniques: table.uniques.map(convertUnique),
|
|
270
314
|
indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
|
|
271
315
|
...ifDefined('checks', checks),
|
|
272
|
-
};
|
|
316
|
+
});
|
|
273
317
|
}
|
|
274
318
|
|
|
275
319
|
/**
|
|
@@ -303,7 +347,7 @@ export function detectDestructiveChanges(
|
|
|
303
347
|
|
|
304
348
|
for (const tableName of Object.keys(fromTables)) {
|
|
305
349
|
const toTableRaw = toNs?.entries.table?.[tableName];
|
|
306
|
-
if (!(toTableRaw
|
|
350
|
+
if (!StorageTable.is(toTableRaw)) {
|
|
307
351
|
conflicts.push({
|
|
308
352
|
kind: 'tableRemoved',
|
|
309
353
|
summary: `Table "${tableName}" was removed`,
|
|
@@ -313,7 +357,7 @@ export function detectDestructiveChanges(
|
|
|
313
357
|
const toTable = toTableRaw;
|
|
314
358
|
|
|
315
359
|
const fromTableRaw = fromTables[tableName];
|
|
316
|
-
if (!(fromTableRaw
|
|
360
|
+
if (!StorageTable.is(fromTableRaw)) continue;
|
|
317
361
|
const fromTable = fromTableRaw;
|
|
318
362
|
|
|
319
363
|
for (const columnName of Object.keys(fromTable.columns)) {
|
|
@@ -358,6 +402,48 @@ export interface ContractToSchemaIROptions {
|
|
|
358
402
|
*
|
|
359
403
|
* Returns an empty schema IR when `contract` is `null` (new project).
|
|
360
404
|
*/
|
|
405
|
+
/**
|
|
406
|
+
* Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
|
|
407
|
+
* table name within that namespace. Unlike {@link contractToSchemaIR}, which
|
|
408
|
+
* flattens every namespace's tables into one bare-keyed record (and throws on a
|
|
409
|
+
* cross-namespace name collision), this scopes the table iteration to one
|
|
410
|
+
* namespace so the same table name can exist in two schemas.
|
|
411
|
+
*
|
|
412
|
+
* The full `storage` is still passed to `convertTable`, so value-set / enum /
|
|
413
|
+
* type resolution that legitimately spans namespaces is unaffected. Foreign
|
|
414
|
+
* keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
|
|
415
|
+
* FKs survive per-namespace conversion. The `annotations` block (storage-type
|
|
416
|
+
* derived) is omitted here — the per-namespace tree consumer reads only the
|
|
417
|
+
* per-table fields.
|
|
418
|
+
*/
|
|
419
|
+
export function contractNamespaceToSchemaIR(
|
|
420
|
+
storage: SqlStorage,
|
|
421
|
+
namespaceId: string,
|
|
422
|
+
options: ContractToSchemaIROptions,
|
|
423
|
+
): SqlSchemaIR {
|
|
424
|
+
if (options.annotationNamespace.length === 0) {
|
|
425
|
+
throw new Error('annotationNamespace must be a non-empty string');
|
|
426
|
+
}
|
|
427
|
+
const namespace = storage.namespaces[namespaceId];
|
|
428
|
+
if (!namespace) {
|
|
429
|
+
return new SqlSchemaIR({ tables: {} });
|
|
430
|
+
}
|
|
431
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
432
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
433
|
+
for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
|
|
434
|
+
StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
|
|
435
|
+
tables[tableName] = convertTable(
|
|
436
|
+
tableName,
|
|
437
|
+
tableDefRaw,
|
|
438
|
+
storageTypes,
|
|
439
|
+
options.expandNativeType,
|
|
440
|
+
options.renderDefault,
|
|
441
|
+
storage,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return new SqlSchemaIR({ tables });
|
|
445
|
+
}
|
|
446
|
+
|
|
361
447
|
export function contractToSchemaIR(
|
|
362
448
|
contract: Contract<SqlStorage> | null,
|
|
363
449
|
options: ContractToSchemaIROptions,
|
|
@@ -367,21 +453,15 @@ export function contractToSchemaIR(
|
|
|
367
453
|
}
|
|
368
454
|
|
|
369
455
|
if (!contract) {
|
|
370
|
-
return { tables: {} };
|
|
456
|
+
return new SqlSchemaIR({ tables: {} });
|
|
371
457
|
}
|
|
372
458
|
|
|
373
459
|
const storage = contract.storage;
|
|
374
|
-
const storageTypes: ResolvedStorageTypes = {
|
|
375
|
-
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
376
|
-
};
|
|
460
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
377
461
|
const tables: Record<string, SqlTableIR> = {};
|
|
378
462
|
for (const ns of Object.values(storage.namespaces)) {
|
|
379
463
|
for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
380
|
-
|
|
381
|
-
throw new Error(
|
|
382
|
-
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
464
|
+
StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
|
|
385
465
|
const tableDef = tableDefRaw;
|
|
386
466
|
if (tables[tableName] !== undefined) {
|
|
387
467
|
throw new Error(
|
|
@@ -405,10 +485,10 @@ export function contractToSchemaIR(
|
|
|
405
485
|
options.resolveEnumNamespaceSchema,
|
|
406
486
|
);
|
|
407
487
|
|
|
408
|
-
return {
|
|
488
|
+
return new SqlSchemaIR({
|
|
409
489
|
tables,
|
|
410
490
|
...ifDefined('annotations', annotations),
|
|
411
|
-
};
|
|
491
|
+
});
|
|
412
492
|
}
|
|
413
493
|
|
|
414
494
|
function deriveAnnotations(
|
|
@@ -418,7 +498,7 @@ function deriveAnnotations(
|
|
|
418
498
|
): SqlAnnotations | undefined {
|
|
419
499
|
const storageTypes: Record<string, StorageTypeInstance> = {};
|
|
420
500
|
|
|
421
|
-
for (const typeInstance of Object.values(
|
|
501
|
+
for (const typeInstance of Object.values(storage.types ?? {})) {
|
|
422
502
|
if (isStorageTypeInstance(typeInstance)) {
|
|
423
503
|
storageTypes[typeInstance.nativeType] = typeInstance;
|
|
424
504
|
}
|
|
@@ -88,8 +88,8 @@ export function planFieldEventOperations(
|
|
|
88
88
|
for (const tableName of tableNames) {
|
|
89
89
|
const priorTableRaw = priorTables?.[tableName];
|
|
90
90
|
const newTableRaw = newTables?.[tableName];
|
|
91
|
-
const priorTable = priorTableRaw
|
|
92
|
-
const newTable = newTableRaw
|
|
91
|
+
const priorTable = StorageTable.is(priorTableRaw) ? priorTableRaw : undefined;
|
|
92
|
+
const newTable = StorageTable.is(newTableRaw) ? newTableRaw : undefined;
|
|
93
93
|
const fieldNames = unionSorted(
|
|
94
94
|
priorTable ? Object.keys(priorTable.columns) : [],
|
|
95
95
|
newTable ? Object.keys(newTable.columns) : [],
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
2
|
+
import { extractCodecControlHooks } from '../assembly';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds the codec-hook-composed `expandNativeType` callback the contract→IR
|
|
6
|
+
* derivation uses to expand parameterized native types (e.g. `character` +
|
|
7
|
+
* `{ length: 36 }` → `character(36)`). Returns `undefined` when no framework
|
|
8
|
+
* components are supplied, so callers can omit the option entirely.
|
|
9
|
+
*/
|
|
10
|
+
export function buildNativeTypeExpander(
|
|
11
|
+
frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>,
|
|
12
|
+
) {
|
|
13
|
+
if (!frameworkComponents) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const codecHooks = extractCodecControlHooks(frameworkComponents);
|
|
17
|
+
return (input: {
|
|
18
|
+
readonly nativeType: string;
|
|
19
|
+
readonly codecId?: string;
|
|
20
|
+
readonly typeParams?: Record<string, unknown>;
|
|
21
|
+
}) => {
|
|
22
|
+
if (!input.typeParams) return input.nativeType;
|
|
23
|
+
if (!input.codecId) return input.nativeType;
|
|
24
|
+
const hooks = codecHooks.get(input.codecId);
|
|
25
|
+
if (!hooks?.expandNativeType) return input.nativeType;
|
|
26
|
+
return hooks.expandNativeType(input);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
|
+
import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
4
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
5
|
+
import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The full-tree node diff a SQL target produces for the family verify
|
|
9
|
+
* verdict: the target derives the expected tree from the contract, applies
|
|
10
|
+
* the pre-diff normalizations (semantic satisfaction, FK schema-segment
|
|
11
|
+
* resolution), runs the generic differ, and ownership-scopes the result.
|
|
12
|
+
* Strict gating, control-policy disposition, and the verdict itself are the
|
|
13
|
+
* family's post-diff filters over this output.
|
|
14
|
+
*/
|
|
15
|
+
export interface SqlSchemaDiffResult {
|
|
16
|
+
/** The full, ownership-scoped diff issue list. */
|
|
17
|
+
readonly issues: readonly SchemaDiffIssue[];
|
|
18
|
+
/**
|
|
19
|
+
* Resolves a diff issue's subject table's declared control policy directly
|
|
20
|
+
* from the contract (Decision 5's own-layer-per-concern discipline extends
|
|
21
|
+
* here too: control policy is a contract concern, resolved by the target
|
|
22
|
+
* at disposition time — never stamped on the diff node). `undefined`
|
|
23
|
+
* when the issue's path resolves to no contract table (a genuine orphan,
|
|
24
|
+
* or a non-table subject).
|
|
25
|
+
*/
|
|
26
|
+
readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The expected/actual namespace-node pairs the codec `verifyType` hooks
|
|
29
|
+
* run over — one per contract namespace with tables, paired by DDL
|
|
30
|
+
* schema; a flat target repeats its sole actual root per such namespace.
|
|
31
|
+
*/
|
|
32
|
+
readonly namespacePairs: ReadonlyArray<{ readonly actual: SqlSchemaIRNode | undefined }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SqlSchemaDiffInput {
|
|
36
|
+
readonly contract: Contract<SqlStorage>;
|
|
37
|
+
readonly schema: SqlSchemaIRNode;
|
|
38
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SqlSchemaDiffFn = (input: SqlSchemaDiffInput) => SqlSchemaDiffResult;
|