@prisma-next/family-sql 0.8.0 → 0.9.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/control-adapter.d.mts +11 -1
- package/dist/control-adapter.d.mts.map +1 -1
- package/dist/control.d.mts +3 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +37 -30
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +135 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +37 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/migration.d.mts +1 -1
- package/dist/runtime.mjs +1 -1
- package/dist/schema-verify.d.mts +2 -1
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/sql-contract-serializer-qUQCnP-k.mjs +125 -0
- package/dist/sql-contract-serializer-qUQCnP-k.mjs.map +1 -0
- package/dist/{timestamp-now-generator-BWp8S2sa.mjs → timestamp-now-generator-r7BP5n3l.mjs} +1 -1
- package/dist/{timestamp-now-generator-BWp8S2sa.mjs.map → timestamp-now-generator-r7BP5n3l.mjs.map} +1 -1
- package/dist/{types-Da-eOg20.d.mts → types-DMINfGUO.d.mts} +37 -25
- package/dist/types-DMINfGUO.d.mts.map +1 -0
- package/dist/{verify-pRYxnpiG.mjs → verify-Crewz6hG.mjs} +1 -1
- package/dist/{verify-pRYxnpiG.mjs.map → verify-Crewz6hG.mjs.map} +1 -1
- package/dist/{verify-sql-schema-DV-UsTG9.mjs → verify-sql-schema-BXw7yx6L.mjs} +53 -7
- package/dist/verify-sql-schema-BXw7yx6L.mjs.map +1 -0
- package/dist/{verify-sql-schema-CPHiuYHR.d.mts → verify-sql-schema-Bfvz07Ik.d.mts} +14 -2
- package/dist/verify-sql-schema-Bfvz07Ik.d.mts.map +1 -0
- package/dist/verify.mjs +1 -1
- package/package.json +22 -21
- package/src/core/control-adapter.ts +14 -0
- package/src/core/control-instance.ts +33 -52
- package/src/core/ir/sql-contract-serializer-base.ts +136 -0
- package/src/core/ir/sql-contract-serializer.ts +18 -0
- package/src/core/ir/sql-schema-verifier-base.ts +56 -0
- package/src/core/migrations/contract-to-schema-ir.ts +71 -22
- package/src/core/migrations/types.ts +25 -5
- package/src/core/schema-verify/verify-sql-schema.ts +117 -21
- package/src/exports/control.ts +1 -1
- package/src/exports/ir.ts +6 -0
- package/dist/types-Da-eOg20.d.mts.map +0 -1
- package/dist/verify-sql-schema-CPHiuYHR.d.mts.map +0 -1
- package/dist/verify-sql-schema-DV-UsTG9.mjs.map +0 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { ContractSerializer } from '@prisma-next/framework-components/control';
|
|
3
|
+
import { SqlStorage, type SqlStorageTypeEntry } from '@prisma-next/sql-contract/types';
|
|
4
|
+
import { validateSqlContractFully } from '@prisma-next/sql-contract/validators';
|
|
5
|
+
import type { JsonObject } from '@prisma-next/utils/json';
|
|
6
|
+
|
|
7
|
+
export type SqlEntityHydrationFactory = (entry: unknown) => SqlStorageTypeEntry;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SQL family `ContractSerializer` abstract base. Carries the SQL-shared
|
|
11
|
+
* deserialization pipeline:
|
|
12
|
+
*
|
|
13
|
+
* 1. `parseSqlContractStructure` validates the on-disk JSON envelope
|
|
14
|
+
* against the SQL contract arktype schema (`validateSqlContractFully`)
|
|
15
|
+
* and returns the validated flat-data shape.
|
|
16
|
+
* 2. `hydrateSqlStorage` walks the validated `storage` subtree and
|
|
17
|
+
* constructs the family-shared SQL Contract IR class hierarchy
|
|
18
|
+
* (`SqlStorage` -> `StorageTable` -> `StorageColumn` / `PrimaryKey`
|
|
19
|
+
* / …). The rest of the contract envelope is JSON-clean primitive
|
|
20
|
+
* data and passes through unchanged.
|
|
21
|
+
* 3. `constructTargetContract` is the target-specific extension hook;
|
|
22
|
+
* defaults to identity. Targets that need to attach target-only
|
|
23
|
+
* fields (e.g. target-specific derived storage fields) override it.
|
|
24
|
+
*
|
|
25
|
+
* Default `serializeContract` is identity over the contract — concrete
|
|
26
|
+
* SQL targets ship JSON-clean class instances, so the contract value
|
|
27
|
+
* can be stringified directly. The non-enumerable family-level `kind`
|
|
28
|
+
* discriminator on `SqlNode` instances stays out of the persisted
|
|
29
|
+
* envelope automatically. Targets that need to canonicalize on the way
|
|
30
|
+
* out (key ordering, dropping computed-only fields) override
|
|
31
|
+
* `serializeContract` directly.
|
|
32
|
+
*/
|
|
33
|
+
export abstract class SqlContractSerializerBase<TContract extends Contract<SqlStorage>>
|
|
34
|
+
implements ContractSerializer<TContract>
|
|
35
|
+
{
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly entityTypeRegistry: ReadonlyMap<string, SqlEntityHydrationFactory>,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
deserializeContract(json: unknown): TContract {
|
|
41
|
+
const validated = this.parseSqlContractStructure(json);
|
|
42
|
+
const hydrated = this.hydrateSqlStorage(validated);
|
|
43
|
+
return this.constructTargetContract(hydrated);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
serializeContract(contract: TContract): JsonObject {
|
|
47
|
+
// Targets that ship enumerable runtime-only fields must override
|
|
48
|
+
// this method (mirroring `MongoTargetContractSerializer.serializeContract`)
|
|
49
|
+
// to construct the persisted envelope explicitly; the default identity
|
|
50
|
+
// works only when every enumerable own property belongs in the JSON.
|
|
51
|
+
return contract as unknown as JsonObject;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Family-shared validation pipeline (delegates to
|
|
56
|
+
* `validateSqlContractFully` in `@prisma-next/sql-contract/validators`):
|
|
57
|
+
* structural arktype + framework-shared domain + SQL storage
|
|
58
|
+
* logical-consistency + SQL storage semantic + model ↔ storage
|
|
59
|
+
* reference checks. Subclasses override to add target-specific
|
|
60
|
+
* structural checks before hydration; the family default suffices
|
|
61
|
+
* for targets whose contract shape is the SQL-shared shape
|
|
62
|
+
* (Postgres, SQLite today).
|
|
63
|
+
*/
|
|
64
|
+
protected parseSqlContractStructure(json: unknown): Contract<SqlStorage> {
|
|
65
|
+
return validateSqlContractFully<Contract<SqlStorage>>(json);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Family-shared hydration walker. Lifts the validated flat-data
|
|
70
|
+
* `storage` subtree into the SQL Contract IR class hierarchy by
|
|
71
|
+
* constructing a single `SqlStorage` instance — its constructor
|
|
72
|
+
* cascades nested instantiation of `StorageTable`, `StorageColumn`,
|
|
73
|
+
* `PrimaryKey`, `UniqueConstraint`, `Index`, `ForeignKey`,
|
|
74
|
+
* `ForeignKeyReferences`, and `StorageTypeInstance`. The rest of the
|
|
75
|
+
* contract envelope (target identity, hashes, capabilities, models,
|
|
76
|
+
* meta, …) is JSON-clean primitive data and passes through unchanged.
|
|
77
|
+
*
|
|
78
|
+
* Polymorphic `storage.types` entries are normalised before the
|
|
79
|
+
* `SqlStorage` constructor runs: when an entry carries an enumerable
|
|
80
|
+
* string `kind`, the serializer looks up a pack-registered hydration
|
|
81
|
+
* factory for that discriminator and delegates reconstruction. Entries
|
|
82
|
+
* with no registered factory pass through unchanged (codec-typed JSON
|
|
83
|
+
* stays codec-typed until `SqlStorage` normalises it).
|
|
84
|
+
*/
|
|
85
|
+
protected hydrateSqlStorage(validated: Contract<SqlStorage>): Contract<SqlStorage> {
|
|
86
|
+
const types = validated.storage.types;
|
|
87
|
+
const hydratedTypes =
|
|
88
|
+
types !== undefined
|
|
89
|
+
? Object.fromEntries(
|
|
90
|
+
Object.entries(types).map(([name, entry]) => [
|
|
91
|
+
name,
|
|
92
|
+
this.hydrateStorageTypeEntry(entry),
|
|
93
|
+
]),
|
|
94
|
+
)
|
|
95
|
+
: undefined;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...validated,
|
|
99
|
+
storage: new SqlStorage({
|
|
100
|
+
...validated.storage,
|
|
101
|
+
...(hydratedTypes !== undefined ? { types: hydratedTypes } : {}),
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Per-entry hydration dispatcher for `storage.types`. When `kind` is a
|
|
108
|
+
* string and the constructor registry supplies a factory for that key,
|
|
109
|
+
* the factory returns the hydrated `SqlStorageTypeEntry`. Otherwise the
|
|
110
|
+
* entry passes through unchanged for `SqlStorage` to normalise.
|
|
111
|
+
*/
|
|
112
|
+
protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {
|
|
113
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
114
|
+
return entry;
|
|
115
|
+
}
|
|
116
|
+
const kind = (entry as { kind?: unknown }).kind;
|
|
117
|
+
if (typeof kind !== 'string') {
|
|
118
|
+
return entry;
|
|
119
|
+
}
|
|
120
|
+
const factory = this.entityTypeRegistry.get(kind);
|
|
121
|
+
if (factory === undefined) {
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
return factory(entry);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Target-specific construction hook. Defaults to identity; targets
|
|
129
|
+
* that need to wrap the hydrated contract (e.g. attach target-only
|
|
130
|
+
* derived fields, narrow the contract type to a target-specific
|
|
131
|
+
* subtype) override.
|
|
132
|
+
*/
|
|
133
|
+
protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
|
|
134
|
+
return hydrated as TContract;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default SQL family `ContractSerializer` concretion. Inherits the
|
|
7
|
+
* full SQL-shared deserialization pipeline (structural validation +
|
|
8
|
+
* IR-class hydration) without pack-registered `storage.types`
|
|
9
|
+
* hydration factories — targets that emit polymorphic JSON outside the
|
|
10
|
+
* codec-typed envelope wire a target-specific subclass with a populated
|
|
11
|
+
* registry (see Postgres). Family-level call sites instantiate this
|
|
12
|
+
* default directly when no target serializer is supplied.
|
|
13
|
+
*/
|
|
14
|
+
export class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(new Map());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SchemaIssue,
|
|
3
|
+
SchemaVerifier,
|
|
4
|
+
SchemaVerifyOptions,
|
|
5
|
+
SchemaVerifyResult,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SQL family `SchemaVerifier` abstract base. Centralises the SQL-shared
|
|
10
|
+
* walk (table-by-table + column-by-column matching keyed by
|
|
11
|
+
* `(namespace.id, name)`, FK / unique / index comparisons via the
|
|
12
|
+
* existing helpers in `verify-helpers.ts`) and exposes a protected hook
|
|
13
|
+
* for target extensions (Postgres functions, RLS policies, future
|
|
14
|
+
* target-only kinds).
|
|
15
|
+
*
|
|
16
|
+
* The base accumulates issues in a single buffer and returns the
|
|
17
|
+
* combined result; the per-SPI family abstract handles the result
|
|
18
|
+
* envelope shape so concrete subclasses focus on target-specific
|
|
19
|
+
* verification logic.
|
|
20
|
+
*
|
|
21
|
+
* The protected hooks (`verifyCommonSqlSchema`,
|
|
22
|
+
* `verifyTargetExtensions`) carry the stable base API that target
|
|
23
|
+
* subclasses (`PostgresSchemaVerifier`, `SqliteSchemaVerifier`)
|
|
24
|
+
* compile against; the SQL-shared walk implementation will be lifted
|
|
25
|
+
* into this base when the verifier behaviour migrates off the
|
|
26
|
+
* legacy adapter shells.
|
|
27
|
+
*/
|
|
28
|
+
export abstract class SqlSchemaVerifierBase<TContract, TSchema>
|
|
29
|
+
implements SchemaVerifier<TContract, TSchema>
|
|
30
|
+
{
|
|
31
|
+
verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult {
|
|
32
|
+
const issues: SchemaIssue[] = [];
|
|
33
|
+
issues.push(...this.verifyCommonSqlSchema(options));
|
|
34
|
+
issues.push(...this.verifyTargetExtensions(options));
|
|
35
|
+
return { ok: issues.length === 0, issues };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SQL-shared verification — table/column/FK/unique/index walks keyed
|
|
40
|
+
* by `(namespace.id, name)`. Concrete subclasses provide the
|
|
41
|
+
* family-shared implementation today; a future iteration will lift
|
|
42
|
+
* the shared walk into this base.
|
|
43
|
+
*/
|
|
44
|
+
protected abstract verifyCommonSqlSchema(
|
|
45
|
+
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
46
|
+
): readonly SchemaIssue[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Target-specific extensions — e.g. Postgres functions, future RLS
|
|
50
|
+
* policies, namespace-mismatch issues. Returns the empty list when the
|
|
51
|
+
* target ships no extensions over the SQL family alphabet.
|
|
52
|
+
*/
|
|
53
|
+
protected abstract verifyTargetExtensions(
|
|
54
|
+
options: SchemaVerifyOptions<TContract, TSchema>,
|
|
55
|
+
): readonly SchemaIssue[];
|
|
56
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { ColumnDefault, Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
|
|
3
|
-
import
|
|
4
|
-
ForeignKey,
|
|
5
|
-
Index,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import {
|
|
4
|
+
type ForeignKey,
|
|
5
|
+
type Index,
|
|
6
|
+
isPostgresEnumStorageEntry,
|
|
7
|
+
isStorageTypeInstance,
|
|
8
|
+
type PostgresEnumStorageEntry,
|
|
9
|
+
type SqlStorage,
|
|
10
|
+
type StorageColumn,
|
|
11
|
+
type StorageTable,
|
|
12
|
+
type StorageTypeInstance,
|
|
13
|
+
toStorageTypeInstance,
|
|
14
|
+
type UniqueConstraint,
|
|
11
15
|
} from '@prisma-next/sql-contract/types';
|
|
12
16
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
13
17
|
import type {
|
|
@@ -51,7 +55,7 @@ export type DefaultRenderer = (def: ColumnDefault, column: StorageColumn) => str
|
|
|
51
55
|
function convertColumn(
|
|
52
56
|
name: string,
|
|
53
57
|
column: StorageColumn,
|
|
54
|
-
storageTypes:
|
|
58
|
+
storageTypes: ResolvedStorageTypes,
|
|
55
59
|
expandNativeType: NativeTypeExpander | undefined,
|
|
56
60
|
renderDefault: DefaultRenderer | undefined,
|
|
57
61
|
): SqlColumnIR {
|
|
@@ -82,9 +86,21 @@ function convertColumn(
|
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
/**
|
|
90
|
+
* `storageTypes` is polymorphic per Decision 18 (Option B) — codec-typed
|
|
91
|
+
* entries match `StorageTypeInstance`; enum entries match the structural
|
|
92
|
+
* `PostgresEnumStorageEntry` shape (Postgres-only; cross-domain layering
|
|
93
|
+
* keeps target IR classes out of the family layer). Both shapes resolve
|
|
94
|
+
* into the same `(codecId, nativeType, typeParams)` triplet at the
|
|
95
|
+
* column-resolution boundary so downstream walks stay uniform.
|
|
96
|
+
*/
|
|
97
|
+
type ResolvedStorageTypes = Readonly<
|
|
98
|
+
Record<string, StorageTypeInstance | PostgresEnumStorageEntry>
|
|
99
|
+
>;
|
|
100
|
+
|
|
85
101
|
function resolveColumnTypeMetadata(
|
|
86
102
|
column: StorageColumn,
|
|
87
|
-
storageTypes:
|
|
103
|
+
storageTypes: ResolvedStorageTypes,
|
|
88
104
|
): Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'> {
|
|
89
105
|
if (!column.typeRef) {
|
|
90
106
|
return column;
|
|
@@ -95,11 +111,23 @@ function resolveColumnTypeMetadata(
|
|
|
95
111
|
`Column references storage type "${column.typeRef}" but it is not defined in storage.types.`,
|
|
96
112
|
);
|
|
97
113
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
if (isPostgresEnumStorageEntry(referenced)) {
|
|
115
|
+
return {
|
|
116
|
+
codecId: referenced.codecId,
|
|
117
|
+
nativeType: referenced.nativeType,
|
|
118
|
+
typeParams: { values: referenced.values } as Record<string, unknown>,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (isStorageTypeInstance(referenced)) {
|
|
122
|
+
return {
|
|
123
|
+
codecId: referenced.codecId,
|
|
124
|
+
nativeType: referenced.nativeType,
|
|
125
|
+
typeParams: referenced.typeParams,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Storage type "${column.typeRef}" has an unknown polymorphic kind; expected codec-instance or postgres-enum.`,
|
|
130
|
+
);
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
function convertUnique(unique: UniqueConstraint): SqlUniqueIR {
|
|
@@ -129,7 +157,7 @@ function convertForeignKey(fk: ForeignKey): SqlForeignKeyIR {
|
|
|
129
157
|
function convertTable(
|
|
130
158
|
name: string,
|
|
131
159
|
table: StorageTable,
|
|
132
|
-
storageTypes:
|
|
160
|
+
storageTypes: ResolvedStorageTypes,
|
|
133
161
|
expandNativeType: NativeTypeExpander | undefined,
|
|
134
162
|
renderDefault: DefaultRenderer | undefined,
|
|
135
163
|
): SqlTableIR {
|
|
@@ -250,7 +278,7 @@ export function contractToSchemaIR(
|
|
|
250
278
|
}
|
|
251
279
|
|
|
252
280
|
const storage = contract.storage;
|
|
253
|
-
const storageTypes = storage.types ?? {};
|
|
281
|
+
const storageTypes = (storage.types ?? {}) as ResolvedStorageTypes;
|
|
254
282
|
const tables: Record<string, SqlTableIR> = {};
|
|
255
283
|
for (const [tableName, tableDef] of Object.entries(storage.tables)) {
|
|
256
284
|
tables[tableName] = convertTable(
|
|
@@ -274,11 +302,32 @@ function deriveAnnotations(
|
|
|
274
302
|
storage: SqlStorage,
|
|
275
303
|
annotationNamespace: string,
|
|
276
304
|
): SqlAnnotations | undefined {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
305
|
+
const types = storage.types as ResolvedStorageTypes | undefined;
|
|
306
|
+
if (!types || Object.keys(types).length === 0) return undefined;
|
|
307
|
+
// Re-key by nativeType, normalising every variant to the codec-typed
|
|
308
|
+
// annotation shape `{codecId, nativeType, typeParams}` produced by the
|
|
309
|
+
// adapter introspector (`introspectPostgresEnumTypes` writes that shape;
|
|
310
|
+
// see also `enum-planning.ts § readExistingEnumValues`, which reads
|
|
311
|
+
// `existing.codecId` + `existing.typeParams.values`). Without this
|
|
312
|
+
// normalisation, the projector would emit the raw
|
|
313
|
+
// `PostgresEnumStorageEntry` shape (top-level `values`, no `typeParams`)
|
|
314
|
+
// and downstream Schema IR consumers that walk the codec-typed shape
|
|
315
|
+
// would see enum entries as new (e.g. the planner emits a fresh
|
|
316
|
+
// `CreateEnumTypeCall` instead of the rebuild recipe). Unknown future
|
|
317
|
+
// kinds without `nativeType` are skipped rather than crashing.
|
|
318
|
+
const byNativeType: Record<string, StorageTypeInstance> = {};
|
|
319
|
+
for (const typeInstance of Object.values(types)) {
|
|
320
|
+
if (isPostgresEnumStorageEntry(typeInstance)) {
|
|
321
|
+
byNativeType[typeInstance.nativeType] = toStorageTypeInstance({
|
|
322
|
+
codecId: typeInstance.codecId,
|
|
323
|
+
nativeType: typeInstance.nativeType,
|
|
324
|
+
typeParams: { values: typeInstance.values },
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (isStorageTypeInstance(typeInstance)) {
|
|
329
|
+
byNativeType[typeInstance.nativeType] = typeInstance;
|
|
330
|
+
}
|
|
282
331
|
}
|
|
283
332
|
return { [annotationNamespace]: { storageTypes: byNativeType } };
|
|
284
333
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
3
|
import type {
|
|
4
|
+
ContractSerializer,
|
|
4
5
|
ContractSpace,
|
|
5
6
|
ControlAdapterDescriptor,
|
|
6
7
|
ControlDriverInstance,
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
OperationContext,
|
|
19
20
|
OpFactoryCall,
|
|
20
21
|
SchemaIssue,
|
|
22
|
+
SchemaVerifier,
|
|
21
23
|
} from '@prisma-next/framework-components/control';
|
|
22
24
|
import type {
|
|
23
25
|
SqlStorage,
|
|
@@ -313,9 +315,10 @@ export interface SqlMigrationPlannerPlanOptions {
|
|
|
313
315
|
* or `null` for reconciliation flows that have no prior contract.
|
|
314
316
|
*
|
|
315
317
|
* Required at every call site so the structural fact "I have a prior
|
|
316
|
-
* contract / I don't" is visible in the type. `migration plan`
|
|
317
|
-
* the
|
|
318
|
-
*
|
|
318
|
+
* contract / I don't" is visible in the type. `migration plan` reads
|
|
319
|
+
* the predecessor bundle's `end-contract.json` from disk and passes
|
|
320
|
+
* the parsed value; `db update` / `db init` reconcile against the
|
|
321
|
+
* live schema and pass `null`. Strategies that
|
|
319
322
|
* need from/to column-shape comparisons (unsafe type change, nullability
|
|
320
323
|
* tightening) use this to decide whether to emit `dataTransform`
|
|
321
324
|
* placeholders; they short-circuit when it is `null`.
|
|
@@ -463,9 +466,26 @@ export interface MultiSpaceRunnerFailure extends SqlMigrationRunnerFailure {
|
|
|
463
466
|
|
|
464
467
|
export type MultiSpaceRunnerResult = Result<MultiSpaceRunnerSuccessValue, MultiSpaceRunnerFailure>;
|
|
465
468
|
|
|
466
|
-
export interface SqlControlTargetDescriptor<
|
|
467
|
-
extends
|
|
469
|
+
export interface SqlControlTargetDescriptor<
|
|
470
|
+
TTargetId extends string,
|
|
471
|
+
TTargetDetails,
|
|
472
|
+
TContract extends Contract<SqlStorage> = Contract<SqlStorage>,
|
|
473
|
+
> extends MigratableTargetDescriptor<'sql', TTargetId, SqlControlFamilyInstance> {
|
|
468
474
|
readonly queryOperations?: () => SqlOperationDescriptors;
|
|
475
|
+
/**
|
|
476
|
+
* JSON ⇄ class boundary for the SQL target's contract. The descriptor
|
|
477
|
+
* composes a concrete `SqlContractSerializerBase` subclass; the rest
|
|
478
|
+
* of the control stack reaches `descriptor.contractSerializer` rather
|
|
479
|
+
* than importing a per-target deserialization function.
|
|
480
|
+
*/
|
|
481
|
+
readonly contractSerializer: ContractSerializer<TContract>;
|
|
482
|
+
/**
|
|
483
|
+
* Per-target schema verifier walking the contract against
|
|
484
|
+
* `SqlSchemaIR`. The descriptor composes a concrete
|
|
485
|
+
* `SqlSchemaVerifierBase` subclass; the family-shared walk lives on
|
|
486
|
+
* the base, the target-specific dispatch on the subclass.
|
|
487
|
+
*/
|
|
488
|
+
readonly schemaVerifier: SchemaVerifier<TContract, SqlSchemaIR>;
|
|
469
489
|
createPlanner(family: SqlControlFamilyInstance): SqlMigrationPlanner<TTargetDetails>;
|
|
470
490
|
createRunner(family: SqlControlFamilyInstance): SqlMigrationRunner<TTargetDetails>;
|
|
471
491
|
}
|
|
@@ -14,10 +14,13 @@ import type {
|
|
|
14
14
|
SchemaVerificationNode,
|
|
15
15
|
VerifyDatabaseSchemaResult,
|
|
16
16
|
} from '@prisma-next/framework-components/control';
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
import {
|
|
18
|
+
isPostgresEnumStorageEntry,
|
|
19
|
+
isStorageTypeInstance,
|
|
20
|
+
type PostgresEnumStorageEntry,
|
|
21
|
+
type SqlStorage,
|
|
22
|
+
type StorageColumn,
|
|
23
|
+
type StorageTypeInstance,
|
|
21
24
|
} from '@prisma-next/sql-contract/types';
|
|
22
25
|
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
23
26
|
import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
|
|
@@ -80,6 +83,21 @@ export interface VerifySqlSchemaOptions {
|
|
|
80
83
|
* with contract native types (e.g., Postgres 'varchar' → 'character varying').
|
|
81
84
|
*/
|
|
82
85
|
readonly normalizeNativeType?: NativeTypeNormalizer;
|
|
86
|
+
/**
|
|
87
|
+
* Bridging adapter that resolves the existing values for a `PostgresEnumStorageEntry`
|
|
88
|
+
* (looked up by its native type) from the introspected schema IR. Targets
|
|
89
|
+
* supply this so the family-level verifier can walk `PostgresEnumStorageEntry` instances
|
|
90
|
+
* natively without reaching into target-specific `schema.annotations`
|
|
91
|
+
* shapes itself.
|
|
92
|
+
*
|
|
93
|
+
* Returning `null` indicates the type is missing from the database; the
|
|
94
|
+
* verifier emits a `type_missing` issue. A non-null array triggers a
|
|
95
|
+
* value-set comparison against the contract's `PostgresEnumStorageEntry.values`.
|
|
96
|
+
*/
|
|
97
|
+
readonly resolveExistingEnumValues?: (
|
|
98
|
+
schema: SqlSchemaIR,
|
|
99
|
+
enumType: PostgresEnumStorageEntry,
|
|
100
|
+
) => readonly string[] | null;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
/**
|
|
@@ -101,6 +119,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
101
119
|
typeMetadataRegistry,
|
|
102
120
|
normalizeDefault,
|
|
103
121
|
normalizeNativeType,
|
|
122
|
+
resolveExistingEnumValues,
|
|
104
123
|
} = options;
|
|
105
124
|
const startTime = Date.now();
|
|
106
125
|
|
|
@@ -109,7 +128,9 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
109
128
|
|
|
110
129
|
const { contractStorageHash, contractProfileHash, contractTarget } =
|
|
111
130
|
extractContractMetadata(contract);
|
|
112
|
-
const storageTypes = contract.storage.types ?? {}
|
|
131
|
+
const storageTypes = (contract.storage.types ?? {}) as Readonly<
|
|
132
|
+
Record<string, PostgresEnumStorageEntry | StorageTypeInstance>
|
|
133
|
+
>;
|
|
113
134
|
const { issues, rootChildren } = verifySchemaTables({
|
|
114
135
|
contract,
|
|
115
136
|
schema,
|
|
@@ -123,15 +144,28 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
123
144
|
|
|
124
145
|
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
125
146
|
|
|
126
|
-
// Verify storage type instances
|
|
147
|
+
// Verify storage type instances. PostgresEnumStorageEntry entries are walked
|
|
148
|
+
// natively (using the bridging adapter `resolveExistingEnumValues`);
|
|
149
|
+
// remaining codec-typed entries continue to dispatch through the
|
|
150
|
+
// generic codec-hook `verifyType` path.
|
|
127
151
|
const storageTypeEntries = Object.entries(storageTypes);
|
|
128
152
|
if (storageTypeEntries.length > 0) {
|
|
129
153
|
const typeNodes: SchemaVerificationNode[] = [];
|
|
130
154
|
for (const [typeName, typeInstance] of storageTypeEntries) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
let typeIssues: readonly SchemaIssue[];
|
|
156
|
+
if (isPostgresEnumStorageEntry(typeInstance)) {
|
|
157
|
+
typeIssues = verifyEnumType({
|
|
158
|
+
typeName,
|
|
159
|
+
typeInstance,
|
|
160
|
+
schema,
|
|
161
|
+
resolveExistingEnumValues,
|
|
162
|
+
});
|
|
163
|
+
} else if (isStorageTypeInstance(typeInstance)) {
|
|
164
|
+
const hook = codecHooks.get(typeInstance.codecId);
|
|
165
|
+
typeIssues = hook?.verifyType ? hook.verifyType({ typeName, typeInstance, schema }) : [];
|
|
166
|
+
} else {
|
|
167
|
+
typeIssues = [];
|
|
168
|
+
}
|
|
135
169
|
if (typeIssues.length > 0) {
|
|
136
170
|
issues.push(...typeIssues);
|
|
137
171
|
}
|
|
@@ -214,6 +248,56 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
214
248
|
|
|
215
249
|
type VerificationStatus = 'pass' | 'warn' | 'fail';
|
|
216
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Native verification walk for `PostgresEnumStorageEntry` instances (no codec hook).
|
|
253
|
+
*
|
|
254
|
+
* Bridges the native `PostgresEnumStorageEntry.values` against the introspected schema
|
|
255
|
+
* IR via the target-supplied `resolveExistingEnumValues` adapter. Without an
|
|
256
|
+
* adapter, the verifier conservatively reports the enum as missing — there
|
|
257
|
+
* is no other way for the family layer to learn about live enum types.
|
|
258
|
+
*/
|
|
259
|
+
function verifyEnumType(options: {
|
|
260
|
+
readonly typeName: string;
|
|
261
|
+
readonly typeInstance: PostgresEnumStorageEntry;
|
|
262
|
+
readonly schema: SqlSchemaIR;
|
|
263
|
+
readonly resolveExistingEnumValues?:
|
|
264
|
+
| ((schema: SqlSchemaIR, enumType: PostgresEnumStorageEntry) => readonly string[] | null)
|
|
265
|
+
| undefined;
|
|
266
|
+
}): readonly SchemaIssue[] {
|
|
267
|
+
const { typeName, typeInstance, schema, resolveExistingEnumValues } = options;
|
|
268
|
+
const desired = typeInstance.values;
|
|
269
|
+
const existing = resolveExistingEnumValues?.(schema, typeInstance) ?? null;
|
|
270
|
+
if (!existing) {
|
|
271
|
+
return [
|
|
272
|
+
{
|
|
273
|
+
kind: 'type_missing',
|
|
274
|
+
typeName,
|
|
275
|
+
message: `Type "${typeName}" is missing from database`,
|
|
276
|
+
},
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
if (arraysEqual(existing, desired)) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
const existingSet = new Set(existing);
|
|
283
|
+
const desiredSet = new Set(desired);
|
|
284
|
+
const addedValues = desired.filter((v) => !existingSet.has(v));
|
|
285
|
+
const removedValues = existing.filter((v) => !desiredSet.has(v));
|
|
286
|
+
const message =
|
|
287
|
+
removedValues.length === 0
|
|
288
|
+
? `Enum type "${typeName}" needs new values: ${addedValues.join(', ')}`
|
|
289
|
+
: `Enum type "${typeName}" values changed (requires rebuild): +[${addedValues.join(', ')}] -[${removedValues.join(', ')}]`;
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
kind: 'enum_values_changed' as const,
|
|
293
|
+
typeName,
|
|
294
|
+
addedValues,
|
|
295
|
+
removedValues,
|
|
296
|
+
message,
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
|
|
217
301
|
function extractContractMetadata(contract: Contract<SqlStorage>): {
|
|
218
302
|
contractStorageHash: SqlStorage['storageHash'];
|
|
219
303
|
contractProfileHash?: Contract<SqlStorage>['profileHash'] | undefined;
|
|
@@ -235,7 +319,7 @@ function verifySchemaTables(options: {
|
|
|
235
319
|
strict: boolean;
|
|
236
320
|
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
|
|
237
321
|
codecHooks: Map<string, CodecControlHooks>;
|
|
238
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
322
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
|
|
239
323
|
normalizeDefault?: DefaultNormalizer;
|
|
240
324
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
241
325
|
}): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
|
|
@@ -329,7 +413,7 @@ function verifyTableChildren(options: {
|
|
|
329
413
|
strict: boolean;
|
|
330
414
|
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
|
|
331
415
|
codecHooks: Map<string, CodecControlHooks>;
|
|
332
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
416
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
|
|
333
417
|
normalizeDefault?: DefaultNormalizer;
|
|
334
418
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
335
419
|
}): SchemaVerificationNode[] {
|
|
@@ -487,7 +571,7 @@ function collectContractColumnNodes(options: {
|
|
|
487
571
|
strict: boolean;
|
|
488
572
|
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
|
|
489
573
|
codecHooks: Map<string, CodecControlHooks>;
|
|
490
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
574
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
|
|
491
575
|
normalizeDefault?: DefaultNormalizer;
|
|
492
576
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
493
577
|
}): SchemaVerificationNode[] {
|
|
@@ -594,7 +678,7 @@ function verifyColumn(options: {
|
|
|
594
678
|
strict: boolean;
|
|
595
679
|
typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
|
|
596
680
|
codecHooks: Map<string, CodecControlHooks>;
|
|
597
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
681
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>;
|
|
598
682
|
normalizeDefault?: DefaultNormalizer;
|
|
599
683
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
600
684
|
}): SchemaVerificationNode {
|
|
@@ -937,7 +1021,7 @@ function validateFrameworkComponentsForExtensions(
|
|
|
937
1021
|
*/
|
|
938
1022
|
function renderExpectedNativeType(
|
|
939
1023
|
contractColumn: Contract<SqlStorage>['storage']['tables'][string]['columns'][string],
|
|
940
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
1024
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>,
|
|
941
1025
|
codecHooks: Map<string, CodecControlHooks>,
|
|
942
1026
|
context?: {
|
|
943
1027
|
readonly tableName: string;
|
|
@@ -967,7 +1051,7 @@ function renderExpectedNativeType(
|
|
|
967
1051
|
|
|
968
1052
|
function resolveContractColumnTypeMetadata(
|
|
969
1053
|
contractColumn: StorageColumn,
|
|
970
|
-
storageTypes: Record<string, StorageTypeInstance
|
|
1054
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance | PostgresEnumStorageEntry>>,
|
|
971
1055
|
context?: {
|
|
972
1056
|
readonly tableName: string;
|
|
973
1057
|
readonly columnName: string;
|
|
@@ -987,11 +1071,23 @@ function resolveContractColumnTypeMetadata(
|
|
|
987
1071
|
);
|
|
988
1072
|
}
|
|
989
1073
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1074
|
+
if (isPostgresEnumStorageEntry(referencedType)) {
|
|
1075
|
+
return {
|
|
1076
|
+
codecId: referencedType.codecId,
|
|
1077
|
+
nativeType: referencedType.nativeType,
|
|
1078
|
+
typeParams: { values: referencedType.values } as Record<string, unknown>,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (isStorageTypeInstance(referencedType)) {
|
|
1082
|
+
return {
|
|
1083
|
+
codecId: referencedType.codecId,
|
|
1084
|
+
nativeType: referencedType.nativeType,
|
|
1085
|
+
typeParams: referencedType.typeParams,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`Storage type "${contractColumn.typeRef}" has an unknown polymorphic kind; expected codec-instance or postgres-enum.`,
|
|
1090
|
+
);
|
|
995
1091
|
}
|
|
996
1092
|
|
|
997
1093
|
/**
|
package/src/exports/control.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type {
|
|
|
13
13
|
} from '@prisma-next/framework-components/control';
|
|
14
14
|
export { assembleAuthoringContributions } from '@prisma-next/framework-components/control';
|
|
15
15
|
export { extractCodecControlHooks } from '../core/assembly';
|
|
16
|
-
export type {
|
|
16
|
+
export type { SqlControlFamilyInstance } from '../core/control-instance';
|
|
17
17
|
export type {
|
|
18
18
|
ContractToSchemaIROptions,
|
|
19
19
|
DefaultRenderer,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SqlContractSerializer } from '../core/ir/sql-contract-serializer';
|
|
2
|
+
export {
|
|
3
|
+
SqlContractSerializerBase,
|
|
4
|
+
type SqlEntityHydrationFactory,
|
|
5
|
+
} from '../core/ir/sql-contract-serializer-base';
|
|
6
|
+
export { SqlSchemaVerifierBase } from '../core/ir/sql-schema-verifier-base';
|