@prisma-next/target-sqlite 0.14.0-dev.49 → 0.14.0-dev.50
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/control.mjs +6 -18
- package/dist/control.mjs.map +1 -1
- package/dist/migration.mjs +2 -2
- package/dist/native-type-normalizer.mjs +13 -1
- package/dist/native-type-normalizer.mjs.map +1 -0
- package/dist/{op-factory-call-CMBTpb4M.mjs → op-factory-call-D_eYsHTp.mjs} +82 -65
- package/dist/op-factory-call-D_eYsHTp.mjs.map +1 -0
- package/dist/op-factory-call.mjs +1 -1
- package/dist/planner-DhvFrgPT.mjs +854 -0
- package/dist/planner-DhvFrgPT.mjs.map +1 -0
- package/dist/{planner-produced-sqlite-migration-BSjMKJhI.mjs → planner-produced-sqlite-migration-DyRmcteq.mjs} +2 -2
- package/dist/{planner-produced-sqlite-migration-BSjMKJhI.mjs.map → planner-produced-sqlite-migration-DyRmcteq.mjs.map} +1 -1
- package/dist/planner-produced-sqlite-migration.mjs +1 -1
- package/dist/planner.d.mts +46 -10
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +1 -1
- package/dist/{sqlite-migration-BdT_3SlM.mjs → sqlite-migration-D93wCdSD.mjs} +2 -2
- package/dist/{sqlite-migration-BdT_3SlM.mjs.map → sqlite-migration-D93wCdSD.mjs.map} +1 -1
- package/package.json +18 -18
- package/src/core/control-target.ts +9 -22
- package/src/core/migrations/column-ddl-rendering.ts +177 -0
- package/src/core/migrations/diff-database-schema.ts +197 -31
- package/src/core/migrations/issue-planner.ts +359 -488
- package/src/core/migrations/operations/tables.ts +107 -67
- package/src/core/migrations/planner-strategies.ts +118 -137
- package/src/core/migrations/planner.ts +112 -37
- package/src/core/sqlite-schema-verifier.ts +6 -3
- package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
- package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
- package/dist/op-factory-call-CMBTpb4M.mjs.map +0 -1
- package/dist/planner-EoQcJGzv.mjs +0 -676
- package/dist/planner-EoQcJGzv.mjs.map +0 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { StorageColumn } from '@prisma-next/sql-contract/types';
|
|
2
|
+
import {
|
|
3
|
+
DdlColumn,
|
|
4
|
+
type DdlTableConstraint,
|
|
5
|
+
ForeignKeyConstraint,
|
|
6
|
+
FunctionColumnDefault,
|
|
7
|
+
LiteralColumnDefault,
|
|
8
|
+
PrimaryKeyConstraint,
|
|
9
|
+
UniqueConstraint,
|
|
10
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
11
|
+
import type { SqlColumnIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
12
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
13
|
+
import type { SqliteColumnSpec } from './operations/shared';
|
|
14
|
+
import { buildColumnDefaultSql, buildColumnTypeSql } from './planner-ddl-builders';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reconstructs the `StorageColumn`-shaped fields `buildColumnTypeSql` /
|
|
18
|
+
* `buildColumnDefaultSql` expect, from a column node's own stamped codec
|
|
19
|
+
* identity (`codecRef` / `codecBaseNativeType`, Decision 5) — never the
|
|
20
|
+
* contract. SQLite's type renderer only uppercases the resolved base type
|
|
21
|
+
* (no parameterized expansion, no named-type quoting), so `typeRef` is
|
|
22
|
+
* deliberately left unset here: setting it would send `buildColumnTypeSql`
|
|
23
|
+
* back through a live `storageTypes` lookup the node's fields have already
|
|
24
|
+
* resolved past, which throws for an unrecognized reference (unlike
|
|
25
|
+
* Postgres's lenient fallback).
|
|
26
|
+
*/
|
|
27
|
+
function columnLike(
|
|
28
|
+
column: SqlColumnIR,
|
|
29
|
+
): Pick<StorageColumn, 'nativeType' | 'codecId' | 'nullable' | 'many' | 'typeParams' | 'default'> {
|
|
30
|
+
if (column.codecRef === undefined || column.codecBaseNativeType === undefined) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`columnLike: expected column "${column.name}" carries no codec identity — the expected tree must be derived via contractToSchemaIR for planning`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
nativeType: column.codecBaseNativeType,
|
|
37
|
+
codecId: column.codecRef.codecId,
|
|
38
|
+
nullable: column.nullable,
|
|
39
|
+
// `column.many` is unset on contract-derived columns (array-ness rides
|
|
40
|
+
// on the `nativeType` `[]` suffix there instead) — `codecRef.many`
|
|
41
|
+
// carries it. Hand-built/introspected columns set `column.many` directly.
|
|
42
|
+
...((column.many ?? column.codecRef.many) !== undefined
|
|
43
|
+
? { many: column.many ?? column.codecRef.many }
|
|
44
|
+
: {}),
|
|
45
|
+
...(column.codecRef.typeParams !== undefined
|
|
46
|
+
? {
|
|
47
|
+
typeParams: blindCast<
|
|
48
|
+
Record<string, unknown>,
|
|
49
|
+
'CodecRef.typeParams is JsonValue-shaped; the DDL builders only ever read it as the Record the contract column originally carried'
|
|
50
|
+
>(column.codecRef.typeParams),
|
|
51
|
+
}
|
|
52
|
+
: {}),
|
|
53
|
+
...(column.resolvedDefault !== undefined ? { default: column.resolvedDefault } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sqliteDefaultToDdlColumnDefault(
|
|
58
|
+
columnDefault: StorageColumn['default'],
|
|
59
|
+
): DdlColumn['default'] {
|
|
60
|
+
if (!columnDefault) return undefined;
|
|
61
|
+
switch (columnDefault.kind) {
|
|
62
|
+
case 'literal':
|
|
63
|
+
return new LiteralColumnDefault(columnDefault.value);
|
|
64
|
+
case 'function':
|
|
65
|
+
// `autoincrement()` is not a DEFAULT clause — SQLite encodes it as
|
|
66
|
+
// `INTEGER PRIMARY KEY AUTOINCREMENT` inline on the column. Skip it
|
|
67
|
+
// here; the renderer also has a defensive guard for the same case.
|
|
68
|
+
if (columnDefault.expression === 'autoincrement()') return undefined;
|
|
69
|
+
return new FunctionColumnDefault(columnDefault.expression);
|
|
70
|
+
default: {
|
|
71
|
+
const exhaustive: never = columnDefault;
|
|
72
|
+
throw new Error(
|
|
73
|
+
`sqliteDefaultToDdlColumnDefault: unhandled kind "${blindCast<{ kind: string }, 'exhaustiveness: surface the unhandled default kind'>(exhaustive).kind}"`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* True when the column is rendered inline as `INTEGER PRIMARY KEY
|
|
81
|
+
* AUTOINCREMENT` — the sole member of the table's primary key with an
|
|
82
|
+
* `autoincrement()` default. Node-based sibling of the retired
|
|
83
|
+
* `isInlineAutoincrementPrimaryKey` (which read the raw `StorageTable`);
|
|
84
|
+
* reads the table/column nodes instead.
|
|
85
|
+
*/
|
|
86
|
+
export function isInlineAutoincrementPrimaryKeyNode(
|
|
87
|
+
table: SqlTableIR,
|
|
88
|
+
column: SqlColumnIR,
|
|
89
|
+
): boolean {
|
|
90
|
+
if (table.primaryKey?.columns.length !== 1) return false;
|
|
91
|
+
if (table.primaryKey.columns[0] !== column.name) return false;
|
|
92
|
+
return (
|
|
93
|
+
column.resolvedDefault?.kind === 'function' &&
|
|
94
|
+
column.resolvedDefault.expression === 'autoincrement()'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds the flat `SqliteColumnSpec` `AddColumnCall` / `RecreateTableCall`
|
|
100
|
+
* need, resolved from the column node's codec identity — the same builders
|
|
101
|
+
* the pre-`plan(start, end)` op-path called, so the output is
|
|
102
|
+
* byte-identical.
|
|
103
|
+
*/
|
|
104
|
+
export function columnSpecFromNode(column: SqlColumnIR, inline: boolean): SqliteColumnSpec {
|
|
105
|
+
const like = columnLike(column);
|
|
106
|
+
const typeSql = buildColumnTypeSql(like, {});
|
|
107
|
+
const defaultSql = buildColumnDefaultSql(like.default);
|
|
108
|
+
return {
|
|
109
|
+
name: column.name,
|
|
110
|
+
typeSql,
|
|
111
|
+
defaultSql,
|
|
112
|
+
nullable: column.nullable,
|
|
113
|
+
...(inline ? { inlineAutoincrementPrimaryKey: true } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Builds the `DdlColumn` the `CreateTableCall` path needs, resolved from the
|
|
119
|
+
* column node's codec identity.
|
|
120
|
+
*/
|
|
121
|
+
export function ddlColumnFromNode(column: SqlColumnIR, inline: boolean): DdlColumn {
|
|
122
|
+
const like = columnLike(column);
|
|
123
|
+
const typeSql = buildColumnTypeSql(like, {});
|
|
124
|
+
if (inline) {
|
|
125
|
+
// `DdlColumn` has no SQLite-specific autoincrement flag, so the full
|
|
126
|
+
// `PRIMARY KEY AUTOINCREMENT` clause is embedded in the `type` string.
|
|
127
|
+
// The DDL renderer (`ddl-renderer.ts`) substring-detects `AUTOINCREMENT`
|
|
128
|
+
// to suppress the normal NOT NULL / PRIMARY KEY / DEFAULT clause rendering
|
|
129
|
+
// and emit the entire type string verbatim. Both sites must stay in sync.
|
|
130
|
+
// The structural fix (a SQLite-specific column option) is tracked in TML-2866.
|
|
131
|
+
return new DdlColumn({ name: column.name, type: `${typeSql} PRIMARY KEY AUTOINCREMENT` });
|
|
132
|
+
}
|
|
133
|
+
const colDefault = sqliteDefaultToDdlColumnDefault(like.default);
|
|
134
|
+
return new DdlColumn({
|
|
135
|
+
name: column.name,
|
|
136
|
+
type: typeSql,
|
|
137
|
+
...(!column.nullable ? { notNull: true } : {}),
|
|
138
|
+
...(colDefault !== undefined ? { default: colDefault } : {}),
|
|
139
|
+
...(column.codecRef !== undefined ? { codecRef: column.codecRef } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds the table-level constraints (PK / unique / FK) for a `CreateTable`
|
|
145
|
+
* path from the table node — the node-sourced sibling of the retired
|
|
146
|
+
* contract-based `tableToDdlParts`'s constraint half.
|
|
147
|
+
*/
|
|
148
|
+
export function tableConstraintsFromNode(
|
|
149
|
+
table: SqlTableIR,
|
|
150
|
+
hasInlinePk: boolean,
|
|
151
|
+
): DdlTableConstraint[] {
|
|
152
|
+
const constraints: DdlTableConstraint[] = [];
|
|
153
|
+
if (table.primaryKey && !hasInlinePk) {
|
|
154
|
+
constraints.push(new PrimaryKeyConstraint({ columns: table.primaryKey.columns }));
|
|
155
|
+
}
|
|
156
|
+
for (const u of table.uniques) {
|
|
157
|
+
constraints.push(
|
|
158
|
+
new UniqueConstraint({
|
|
159
|
+
columns: u.columns,
|
|
160
|
+
...(u.name !== undefined ? { name: u.name } : {}),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
for (const fk of table.foreignKeys) {
|
|
165
|
+
constraints.push(
|
|
166
|
+
new ForeignKeyConstraint({
|
|
167
|
+
columns: fk.columns,
|
|
168
|
+
refTable: fk.referencedTable,
|
|
169
|
+
refColumns: fk.referencedColumns,
|
|
170
|
+
...(fk.name !== undefined ? { name: fk.name } : {}),
|
|
171
|
+
...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
|
|
172
|
+
...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return constraints;
|
|
177
|
+
}
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
-
import type { ColumnDefault, Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { ColumnDefault, Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type { NativeTypeExpander, SqlSchemaDiffResult } from '@prisma-next/family-sql/control';
|
|
3
|
+
import { buildNativeTypeExpander, contractToSchemaIR } from '@prisma-next/family-sql/control';
|
|
4
|
+
import {
|
|
5
|
+
neutralizeFlatExpectedFkSchemas,
|
|
6
|
+
normalizeFlatActualForDiff,
|
|
7
|
+
verifySqlSchemaByDiff,
|
|
8
|
+
} from '@prisma-next/family-sql/diff';
|
|
4
9
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import type {
|
|
11
|
+
SchemaDiffIssue,
|
|
12
|
+
VerifyDatabaseSchemaResult,
|
|
13
|
+
} from '@prisma-next/framework-components/control';
|
|
14
|
+
import { diffSchemas } from '@prisma-next/framework-components/control';
|
|
15
|
+
import { entityAt } from '@prisma-next/framework-components/ir';
|
|
16
|
+
import type { SqlStorage, StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
|
|
17
|
+
import type {
|
|
18
|
+
SqlColumnIRInput,
|
|
19
|
+
SqlSchemaIRInput,
|
|
20
|
+
SqlSchemaIRNode,
|
|
21
|
+
SqlTableIRInput,
|
|
22
|
+
} from '@prisma-next/sql-schema-ir/types';
|
|
23
|
+
import { relationalNodeGranularity, SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
24
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
25
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
11
26
|
import { renderDefaultLiteral } from './planner-ddl-builders';
|
|
12
27
|
|
|
13
28
|
interface SqliteDiffDatabaseSchemaInput {
|
|
@@ -29,46 +44,197 @@ export function sqliteRenderDefault(def: ColumnDefault, _column: StorageColumn):
|
|
|
29
44
|
return renderDefaultLiteral(def.value);
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The SQLite expected-side projection: contract → flat relational schema IR.
|
|
49
|
+
*
|
|
50
|
+
* `extras` thread the plan-time derivation input: the native-type expander,
|
|
51
|
+
* so the expected side carries resolved native types (like the verify
|
|
52
|
+
* side). Every expected column also carries its `codecRef` unconditionally
|
|
53
|
+
* (Decision 5) — the planner's op-builders resolve DDL rendering from it at
|
|
54
|
+
* plan time, so no separate render stamper is threaded here.
|
|
55
|
+
*/
|
|
56
|
+
export function sqliteContractToSchema(
|
|
57
|
+
contract: Contract<SqlStorage> | null,
|
|
58
|
+
extras?: {
|
|
59
|
+
readonly expandNativeType?: NativeTypeExpander;
|
|
60
|
+
},
|
|
61
|
+
): SqlSchemaIR {
|
|
34
62
|
return contractToSchemaIR(contract, {
|
|
35
63
|
annotationNamespace: 'sqlite',
|
|
36
64
|
renderDefault: sqliteRenderDefault,
|
|
65
|
+
...ifDefined('expandNativeType', extras?.expandNativeType),
|
|
37
66
|
});
|
|
38
67
|
}
|
|
39
68
|
|
|
40
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The SQLite schema verify: the full-tree node-diff verdict wrapped in the
|
|
71
|
+
* issue-based result envelope. Used by the runner's post-apply check; the
|
|
72
|
+
* family `verifySchema` runs the same composition via the descriptor hook.
|
|
73
|
+
*/
|
|
74
|
+
export function verifySqliteDatabaseSchema(
|
|
41
75
|
input: SqliteDiffDatabaseSchemaInput,
|
|
42
76
|
): VerifyDatabaseSchemaResult {
|
|
43
|
-
return
|
|
77
|
+
return verifySqlSchemaByDiff({
|
|
44
78
|
contract: input.contract,
|
|
45
|
-
|
|
46
|
-
buildExpectedSchema: sqliteContractToSchema,
|
|
79
|
+
schema: input.actualSchema,
|
|
47
80
|
strict: input.strict,
|
|
48
|
-
typeMetadataRegistry: input.typeMetadataRegistry,
|
|
49
81
|
frameworkComponents: input.frameworkComponents,
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
diffSchema: diffSqliteSchema,
|
|
83
|
+
granularityOf: relationalNodeGranularity,
|
|
52
84
|
});
|
|
53
85
|
}
|
|
54
86
|
|
|
55
87
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
88
|
+
* Resolves a verdict-diff issue's subject table's declared control policy
|
|
89
|
+
* directly from the contract. SQLite's expected tree is flat, so the issue
|
|
90
|
+
* path carries no namespace segment — `path[1]` is the table name (`path[0]`
|
|
91
|
+
* is the tree root's own id); every contract namespace is searched for that
|
|
92
|
+
* table name (table names are globally unique in the flat tree — duplicates
|
|
93
|
+
* across namespaces are rejected earlier, at `contractToSchemaIR`).
|
|
59
94
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
function resolveControlPolicy(
|
|
96
|
+
issue: SchemaDiffIssue,
|
|
97
|
+
contract: Contract<SqlStorage>,
|
|
98
|
+
): ControlPolicy | undefined {
|
|
99
|
+
const tableName = issue.path[1];
|
|
100
|
+
if (tableName === undefined) return undefined;
|
|
101
|
+
for (const namespaceId of Object.keys(contract.storage.namespaces)) {
|
|
102
|
+
const table = entityAt<StorageTable>(contract.storage, {
|
|
103
|
+
namespaceId,
|
|
104
|
+
entityKind: 'table',
|
|
105
|
+
entityName: tableName,
|
|
106
|
+
});
|
|
107
|
+
if (table !== undefined) return table.control;
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
63
110
|
}
|
|
64
111
|
|
|
65
112
|
/**
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
68
|
-
*
|
|
113
|
+
* The SQLite full-tree node diff for the family verify verdict: derive the
|
|
114
|
+
* expected flat tree with resolved leaf values (expander threaded so
|
|
115
|
+
* parameterized types compare expanded), neutralize the FK schema segment
|
|
116
|
+
* (single-schema target — introspection stamps none), normalize the actual
|
|
117
|
+
* tree for semantic satisfaction, and run the generic differ. Flat targets
|
|
118
|
+
* need no ownership scoping. The codec `verifyType` hooks run once per
|
|
119
|
+
* contract namespace with tables, each against the sole flat actual root —
|
|
120
|
+
* exactly the legacy per-namespace pairing.
|
|
69
121
|
*/
|
|
70
|
-
export function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
export function diffSqliteSchema(input: {
|
|
123
|
+
readonly contract: Contract<SqlStorage>;
|
|
124
|
+
readonly schema: SqlSchemaIRNode;
|
|
125
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
126
|
+
}): SqlSchemaDiffResult {
|
|
127
|
+
const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);
|
|
128
|
+
const expected = neutralizeFlatExpectedFkSchemas(
|
|
129
|
+
contractToSchemaIR(input.contract, {
|
|
130
|
+
annotationNamespace: 'sqlite',
|
|
131
|
+
renderDefault: sqliteRenderDefault,
|
|
132
|
+
...ifDefined('expandNativeType', expandNativeType),
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
const actual =
|
|
136
|
+
input.schema instanceof SqlSchemaIR
|
|
137
|
+
? input.schema
|
|
138
|
+
: blindCast<
|
|
139
|
+
SqlSchemaIR,
|
|
140
|
+
'the SQLite introspection adapter always produces a flat SqlSchemaIR root'
|
|
141
|
+
>(input.schema);
|
|
142
|
+
const normalizedActual = normalizeFlatActualForDiff(expected, actual);
|
|
143
|
+
const issues = diffSchemas(expected, normalizedActual);
|
|
144
|
+
const namespacesWithTables = Object.values(input.contract.storage.namespaces).filter(
|
|
145
|
+
(ns) => Object.keys(ns.entries.table ?? {}).length > 0,
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
issues,
|
|
149
|
+
resolveControlPolicy: (issue) => resolveControlPolicy(issue, input.contract),
|
|
150
|
+
namespacePairs: namespacesWithTables.map(() => ({ actual })),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface SqlitePlanDiff {
|
|
155
|
+
/** The desired ("end") tree — resolved leaf values, incl. `codecRef`, on every column. */
|
|
156
|
+
readonly expected: SqlSchemaIR;
|
|
157
|
+
/** The live ("start") tree, normalized for semantic satisfaction against `expected`. */
|
|
158
|
+
readonly actual: SqlSchemaIR;
|
|
159
|
+
readonly issues: readonly SchemaDiffIssue[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The SQLite planner's diff input: the same tree-building
|
|
164
|
+
* `diffSqliteSchema` uses (expander threaded, FK schema segment
|
|
165
|
+
* neutralized, actual tree normalized for semantic satisfaction). One differ
|
|
166
|
+
* drives both verify and plan; this is the plan-side derivation — column DDL
|
|
167
|
+
* resolves from each expected column's `codecRef` at plan time
|
|
168
|
+
* (`column-ddl-rendering.ts`), so no separate render stamping happens here.
|
|
169
|
+
*/
|
|
170
|
+
export function buildSqlitePlanDiff(input: {
|
|
171
|
+
readonly contract: Contract<SqlStorage>;
|
|
172
|
+
readonly actualSchema: SqlSchemaIRNode;
|
|
173
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
174
|
+
}): SqlitePlanDiff {
|
|
175
|
+
const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);
|
|
176
|
+
const expected = neutralizeFlatExpectedFkSchemas(
|
|
177
|
+
sqliteContractToSchema(input.contract, {
|
|
178
|
+
...ifDefined('expandNativeType', expandNativeType),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
// The differ dispatches polymorphically (`.isEqualTo()` / `.children()`), so
|
|
182
|
+
// the actual tree must be genuine `SqlSchemaIR`/`SqlTableIR`/`SqlColumnIR`
|
|
183
|
+
// instances, not plain data shaped like them. `new SqlSchemaIR(...)`
|
|
184
|
+
// normalizes either input uniformly (an already-real tree passes through
|
|
185
|
+
// untouched — its nested values are already instances) and is a no-op
|
|
186
|
+
// rebuild in the common (real-instance) case, so this is always safe to run.
|
|
187
|
+
const actualRaw = new SqlSchemaIR(withRecordKeyNames(input.actualSchema));
|
|
188
|
+
const actual = normalizeFlatActualForDiff(expected, actualRaw);
|
|
189
|
+
const issues = diffSchemas(expected, actual);
|
|
190
|
+
return { expected, actual, issues };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Every schema-tree builder in this codebase derives a table's / column's
|
|
195
|
+
* `name` from the record key it's stored under (`contractToSchemaIR`,
|
|
196
|
+
* the SQLite introspection adapter) rather than trusting a redundant
|
|
197
|
+
* embedded field — the record key IS the identity. Mirrors that discipline
|
|
198
|
+
* for the actual/live tree before construction, so `SqlTableIR.id` /
|
|
199
|
+
* `SqlColumnIR.id` (both derived from `.name`) are always correct without
|
|
200
|
+
* requiring every caller to duplicate the key onto the value. A no-op for a
|
|
201
|
+
* tree that already carries matching names (the real introspection adapter
|
|
202
|
+
* always does).
|
|
203
|
+
*/
|
|
204
|
+
function withRecordKeyNames(actualSchema: SqlSchemaIRNode): SqlSchemaIRInput {
|
|
205
|
+
const raw = blindCast<
|
|
206
|
+
{ readonly tables?: Readonly<Record<string, unknown>> },
|
|
207
|
+
'the SQLite introspection adapter always produces a flat, tables-keyed root'
|
|
208
|
+
>(actualSchema);
|
|
209
|
+
const tables: Record<string, SqlTableIRInput> = {};
|
|
210
|
+
for (const [tableName, table] of Object.entries(raw.tables ?? {})) {
|
|
211
|
+
const rawTable = blindCast<
|
|
212
|
+
Omit<SqlTableIRInput, 'name' | 'columns'>,
|
|
213
|
+
'every table value in a tables record is SqlTableIR(Input)-shaped'
|
|
214
|
+
>(table);
|
|
215
|
+
const columns: Record<string, SqlColumnIRInput> = {};
|
|
216
|
+
for (const [columnName, column] of Object.entries(
|
|
217
|
+
blindCast<
|
|
218
|
+
{ readonly columns?: Readonly<Record<string, unknown>> },
|
|
219
|
+
'every SqlTableIR(Input) carries a columns record keyed by column name'
|
|
220
|
+
>(table).columns ?? {},
|
|
221
|
+
)) {
|
|
222
|
+
columns[columnName] = {
|
|
223
|
+
...blindCast<
|
|
224
|
+
SqlColumnIRInput,
|
|
225
|
+
'every column value in a columns record is SqlColumnIR(Input)-shaped'
|
|
226
|
+
>(column),
|
|
227
|
+
name: columnName,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
tables[tableName] = {
|
|
231
|
+
...rawTable,
|
|
232
|
+
name: tableName,
|
|
233
|
+
columns,
|
|
234
|
+
foreignKeys: rawTable.foreignKeys ?? [],
|
|
235
|
+
uniques: rawTable.uniques ?? [],
|
|
236
|
+
indexes: rawTable.indexes ?? [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { tables };
|
|
74
240
|
}
|