@prisma-next/target-sqlite 0.5.0-dev.8 → 0.5.0-dev.81
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/codec-ids-CYwMu3-4.d.mts +13 -0
- package/dist/codec-ids-CYwMu3-4.d.mts.map +1 -0
- package/dist/codec-ids-CuUxYcd0.mjs +13 -0
- package/dist/codec-ids-CuUxYcd0.mjs.map +1 -0
- package/dist/codec-ids.d.mts +2 -0
- package/dist/codec-ids.mjs +2 -0
- package/dist/codec-types-DNauB5UT.d.mts +23 -0
- package/dist/codec-types-DNauB5UT.d.mts.map +1 -0
- package/dist/codec-types.d.mts +3 -0
- package/dist/codec-types.mjs +2 -0
- package/dist/codecs-BAlEiSeP.d.mts +126 -0
- package/dist/codecs-BAlEiSeP.d.mts.map +1 -0
- package/dist/codecs-DVnHtVWW.mjs +220 -0
- package/dist/codecs-DVnHtVWW.mjs.map +1 -0
- package/dist/codecs.d.mts +2 -0
- package/dist/codecs.mjs +13 -0
- package/dist/codecs.mjs.map +1 -0
- package/dist/control.d.mts +4 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +428 -5
- package/dist/control.mjs.map +1 -1
- package/dist/default-normalizer-3Fccw7yw.mjs +69 -0
- package/dist/default-normalizer-3Fccw7yw.mjs.map +1 -0
- package/dist/default-normalizer.d.mts +7 -0
- package/dist/default-normalizer.d.mts.map +1 -0
- package/dist/default-normalizer.mjs +2 -0
- package/dist/descriptor-meta-CE2Kbn9b.mjs +17 -0
- package/dist/descriptor-meta-CE2Kbn9b.mjs.map +1 -0
- package/dist/migration.d.mts +85 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +49 -0
- package/dist/migration.mjs.map +1 -0
- package/dist/native-type-normalizer-BlN5XfD-.mjs +14 -0
- package/dist/native-type-normalizer-BlN5XfD-.mjs.map +1 -0
- package/dist/native-type-normalizer.d.mts +11 -0
- package/dist/native-type-normalizer.d.mts.map +1 -0
- package/dist/native-type-normalizer.mjs +2 -0
- package/dist/op-factory-call-DRKKURAO.mjs +279 -0
- package/dist/op-factory-call-DRKKURAO.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +151 -0
- package/dist/op-factory-call.d.mts.map +1 -0
- package/dist/op-factory-call.mjs +2 -0
- package/dist/pack.d.mts +35 -1
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +2 -3
- package/dist/planner-CdCU0v1B.mjs +525 -0
- package/dist/planner-CdCU0v1B.mjs.map +1 -0
- package/dist/planner-produced-sqlite-migration-CI9LdXPr.d.mts +29 -0
- package/dist/planner-produced-sqlite-migration-CI9LdXPr.d.mts.map +1 -0
- package/dist/planner-produced-sqlite-migration-C_TzWbT0.mjs +110 -0
- package/dist/planner-produced-sqlite-migration-C_TzWbT0.mjs.map +1 -0
- package/dist/planner-produced-sqlite-migration.d.mts +2 -0
- package/dist/planner-produced-sqlite-migration.mjs +2 -0
- package/dist/planner-target-details-Bm71XPKb.mjs +15 -0
- package/dist/planner-target-details-Bm71XPKb.mjs.map +1 -0
- package/dist/planner-target-details-vhvZDWK1.d.mts +12 -0
- package/dist/planner-target-details-vhvZDWK1.d.mts.map +1 -0
- package/dist/planner-target-details.d.mts +2 -0
- package/dist/planner-target-details.mjs +2 -0
- package/dist/planner.d.mts +59 -0
- package/dist/planner.d.mts.map +1 -0
- package/dist/planner.mjs +2 -0
- package/dist/render-ops-CSRDT4YL.mjs +8 -0
- package/dist/render-ops-CSRDT4YL.mjs.map +1 -0
- package/dist/render-ops.d.mts +10 -0
- package/dist/render-ops.d.mts.map +1 -0
- package/dist/render-ops.mjs +2 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +4 -8
- package/dist/runtime.mjs.map +1 -1
- package/dist/shared-qLsgTOZs.d.mts +69 -0
- package/dist/shared-qLsgTOZs.d.mts.map +1 -0
- package/dist/sql-utils-DhevMgef.mjs +35 -0
- package/dist/sql-utils-DhevMgef.mjs.map +1 -0
- package/dist/sql-utils.d.mts +22 -0
- package/dist/sql-utils.d.mts.map +1 -0
- package/dist/sql-utils.mjs +2 -0
- package/dist/sqlite-migration-BBJktVVw.mjs +16 -0
- package/dist/sqlite-migration-BBJktVVw.mjs.map +1 -0
- package/dist/sqlite-migration-DAb2NEX6.d.mts +17 -0
- package/dist/sqlite-migration-DAb2NEX6.d.mts.map +1 -0
- package/dist/statement-builders-Dne-LkAV.mjs +158 -0
- package/dist/statement-builders-Dne-LkAV.mjs.map +1 -0
- package/dist/statement-builders.d.mts +68 -0
- package/dist/statement-builders.d.mts.map +1 -0
- package/dist/statement-builders.mjs +2 -0
- package/dist/tables-D84zfPZI.mjs +403 -0
- package/dist/tables-D84zfPZI.mjs.map +1 -0
- package/package.json +33 -11
- package/src/core/authoring.ts +9 -0
- package/src/core/codec-helpers.ts +11 -0
- package/src/core/codec-ids.ts +13 -0
- package/src/core/codecs.ts +337 -0
- package/src/core/control-target.ts +54 -11
- package/src/core/default-normalizer.ts +92 -0
- package/src/core/descriptor-meta.ts +5 -1
- package/src/core/migrations/issue-planner.ts +586 -0
- package/src/core/migrations/op-factory-call.ts +369 -0
- package/src/core/migrations/operations/columns.ts +62 -0
- package/src/core/migrations/operations/data-transform.ts +51 -0
- package/src/core/migrations/operations/indexes.ts +52 -0
- package/src/core/migrations/operations/raw.ts +12 -0
- package/src/core/migrations/operations/shared.ts +120 -0
- package/src/core/migrations/operations/tables.ts +388 -0
- package/src/core/migrations/planner-ddl-builders.ts +142 -0
- package/src/core/migrations/planner-produced-sqlite-migration.ts +70 -0
- package/src/core/migrations/planner-strategies.ts +231 -0
- package/src/core/migrations/planner-target-details.ts +33 -0
- package/src/core/migrations/planner.ts +183 -0
- package/src/core/migrations/render-ops.ts +15 -0
- package/src/core/migrations/render-typescript.ts +91 -0
- package/src/core/migrations/runner.ts +724 -0
- package/src/core/migrations/sqlite-migration.ts +13 -0
- package/src/core/migrations/statement-builders.ts +212 -0
- package/src/core/native-type-normalizer.ts +9 -0
- package/src/core/registry.ts +11 -0
- package/src/core/runtime-target.ts +1 -3
- package/src/core/sql-utils.ts +47 -0
- package/src/exports/codec-ids.ts +13 -0
- package/src/exports/codec-types.ts +43 -0
- package/src/exports/codecs.ts +20 -0
- package/src/exports/control.ts +1 -0
- package/src/exports/default-normalizer.ts +1 -0
- package/src/exports/migration.ts +24 -0
- package/src/exports/native-type-normalizer.ts +1 -0
- package/src/exports/op-factory-call.ts +12 -0
- package/src/exports/planner-produced-sqlite-migration.ts +4 -0
- package/src/exports/planner-target-details.ts +2 -0
- package/src/exports/planner.ts +2 -0
- package/src/exports/render-ops.ts +1 -0
- package/src/exports/sql-utils.ts +1 -0
- package/src/exports/statement-builders.ts +12 -0
- package/dist/descriptor-meta-DbuuziYA.mjs +0 -14
- package/dist/descriptor-meta-DbuuziYA.mjs.map +0 -1
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite migration issue planner.
|
|
3
|
+
*
|
|
4
|
+
* Takes schema issues (from `verifySqlSchema`) and emits migration IR
|
|
5
|
+
* (`SqliteOpFactoryCall[]`). Strategies consume issues they recognize and
|
|
6
|
+
* produce specialized call sequences (e.g. recreateTableStrategy absorbs
|
|
7
|
+
* type/nullability/default/constraint mismatches into a single recreate op);
|
|
8
|
+
* remaining issues flow through `mapIssueToCall` for the default case.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
12
|
+
import type {
|
|
13
|
+
CodecControlHooks,
|
|
14
|
+
MigrationOperationPolicy,
|
|
15
|
+
SqlPlannerConflict,
|
|
16
|
+
SqlPlannerConflictLocation,
|
|
17
|
+
} from '@prisma-next/family-sql/control';
|
|
18
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
19
|
+
import type { SchemaIssue } from '@prisma-next/framework-components/control';
|
|
20
|
+
import type {
|
|
21
|
+
SqlStorage,
|
|
22
|
+
StorageColumn,
|
|
23
|
+
StorageTable,
|
|
24
|
+
StorageTypeInstance,
|
|
25
|
+
} from '@prisma-next/sql-contract/types';
|
|
26
|
+
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
27
|
+
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
28
|
+
import type { Result } from '@prisma-next/utils/result';
|
|
29
|
+
import { notOk, ok } from '@prisma-next/utils/result';
|
|
30
|
+
import {
|
|
31
|
+
AddColumnCall,
|
|
32
|
+
CreateIndexCall,
|
|
33
|
+
CreateTableCall,
|
|
34
|
+
DropColumnCall,
|
|
35
|
+
DropIndexCall,
|
|
36
|
+
DropTableCall,
|
|
37
|
+
type SqliteOpFactoryCall,
|
|
38
|
+
} from './op-factory-call';
|
|
39
|
+
import type {
|
|
40
|
+
SqliteColumnSpec,
|
|
41
|
+
SqliteForeignKeySpec,
|
|
42
|
+
SqliteTableSpec,
|
|
43
|
+
SqliteUniqueSpec,
|
|
44
|
+
} from './operations/shared';
|
|
45
|
+
import {
|
|
46
|
+
buildColumnDefaultSql,
|
|
47
|
+
buildColumnTypeSql,
|
|
48
|
+
isInlineAutoincrementPrimaryKey,
|
|
49
|
+
} from './planner-ddl-builders';
|
|
50
|
+
import {
|
|
51
|
+
type CallMigrationStrategy,
|
|
52
|
+
type StrategyContext,
|
|
53
|
+
sqlitePlannerStrategies,
|
|
54
|
+
} from './planner-strategies';
|
|
55
|
+
import { CONTROL_TABLE_NAMES } from './statement-builders';
|
|
56
|
+
|
|
57
|
+
export type { CallMigrationStrategy, StrategyContext };
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Issue kind ordering (dependency order)
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
const ISSUE_KIND_ORDER: Record<string, number> = {
|
|
64
|
+
// Drops (reconciliation — clear the way for creates)
|
|
65
|
+
extra_foreign_key: 10,
|
|
66
|
+
extra_unique_constraint: 11,
|
|
67
|
+
extra_primary_key: 12,
|
|
68
|
+
extra_index: 13,
|
|
69
|
+
extra_default: 14,
|
|
70
|
+
extra_column: 15,
|
|
71
|
+
extra_table: 16,
|
|
72
|
+
|
|
73
|
+
// Tables before columns
|
|
74
|
+
missing_table: 20,
|
|
75
|
+
|
|
76
|
+
// Columns before constraints
|
|
77
|
+
missing_column: 30,
|
|
78
|
+
|
|
79
|
+
// Reconciliation alters (on existing objects)
|
|
80
|
+
type_mismatch: 40,
|
|
81
|
+
nullability_mismatch: 41,
|
|
82
|
+
default_missing: 42,
|
|
83
|
+
default_mismatch: 43,
|
|
84
|
+
|
|
85
|
+
// Constraints after columns exist
|
|
86
|
+
primary_key_mismatch: 50,
|
|
87
|
+
unique_constraint_mismatch: 51,
|
|
88
|
+
index_mismatch: 52,
|
|
89
|
+
foreign_key_mismatch: 60,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function issueOrder(issue: SchemaIssue): number {
|
|
93
|
+
return ISSUE_KIND_ORDER[issue.kind] ?? 99;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function issueKey(issue: SchemaIssue): string {
|
|
97
|
+
const table = 'table' in issue && typeof issue.table === 'string' ? issue.table : '';
|
|
98
|
+
const column = 'column' in issue && typeof issue.column === 'string' ? issue.column : '';
|
|
99
|
+
const name =
|
|
100
|
+
'indexOrConstraint' in issue && typeof issue.indexOrConstraint === 'string'
|
|
101
|
+
? issue.indexOrConstraint
|
|
102
|
+
: '';
|
|
103
|
+
return `${table}\u0000${column}\u0000${name}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Conflict helpers
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
function issueConflict(
|
|
111
|
+
kind: SqlPlannerConflict['kind'],
|
|
112
|
+
summary: string,
|
|
113
|
+
location?: SqlPlannerConflict['location'],
|
|
114
|
+
): SqlPlannerConflict {
|
|
115
|
+
return {
|
|
116
|
+
kind,
|
|
117
|
+
summary,
|
|
118
|
+
why: 'Use `migration new` to author a custom migration for this change.',
|
|
119
|
+
...(location ? { location } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function conflictKindForIssue(issue: SchemaIssue): SqlPlannerConflict['kind'] {
|
|
124
|
+
switch (issue.kind) {
|
|
125
|
+
case 'type_mismatch':
|
|
126
|
+
return 'typeMismatch';
|
|
127
|
+
case 'nullability_mismatch':
|
|
128
|
+
return 'nullabilityConflict';
|
|
129
|
+
case 'primary_key_mismatch':
|
|
130
|
+
case 'unique_constraint_mismatch':
|
|
131
|
+
case 'index_mismatch':
|
|
132
|
+
case 'extra_primary_key':
|
|
133
|
+
case 'extra_unique_constraint':
|
|
134
|
+
return 'indexIncompatible';
|
|
135
|
+
case 'foreign_key_mismatch':
|
|
136
|
+
case 'extra_foreign_key':
|
|
137
|
+
return 'foreignKeyConflict';
|
|
138
|
+
default:
|
|
139
|
+
return 'missingButNonAdditive';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function issueLocation(issue: SchemaIssue): SqlPlannerConflictLocation | undefined {
|
|
144
|
+
if (issue.kind === 'enum_values_changed') return undefined;
|
|
145
|
+
const location: {
|
|
146
|
+
table?: string;
|
|
147
|
+
column?: string;
|
|
148
|
+
constraint?: string;
|
|
149
|
+
} = {};
|
|
150
|
+
if (issue.table) location.table = issue.table;
|
|
151
|
+
if (issue.column) location.column = issue.column;
|
|
152
|
+
if (issue.indexOrConstraint) location.constraint = issue.indexOrConstraint;
|
|
153
|
+
return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function conflictForDisallowedCall(
|
|
157
|
+
call: SqliteOpFactoryCall,
|
|
158
|
+
allowed: readonly string[],
|
|
159
|
+
): SqlPlannerConflict {
|
|
160
|
+
const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(', ')}`;
|
|
161
|
+
const location = locationForCall(call);
|
|
162
|
+
return {
|
|
163
|
+
kind: conflictKindForCall(call),
|
|
164
|
+
summary,
|
|
165
|
+
why: 'Use `migration new` to author a custom migration for this change.',
|
|
166
|
+
...(location ? { location } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function conflictKindForCall(call: SqliteOpFactoryCall): SqlPlannerConflict['kind'] {
|
|
171
|
+
switch (call.factoryName) {
|
|
172
|
+
case 'createIndex':
|
|
173
|
+
case 'dropIndex':
|
|
174
|
+
return 'indexIncompatible';
|
|
175
|
+
default:
|
|
176
|
+
return 'missingButNonAdditive';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function locationForCall(call: SqliteOpFactoryCall): SqlPlannerConflictLocation | undefined {
|
|
181
|
+
const location: { table?: string; column?: string; index?: string } = {};
|
|
182
|
+
if ('tableName' in call) location.table = call.tableName;
|
|
183
|
+
if ('columnName' in call) location.column = call.columnName;
|
|
184
|
+
if ('indexName' in call) location.index = call.indexName;
|
|
185
|
+
return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isMissing(issue: SchemaIssue): boolean {
|
|
189
|
+
if (issue.kind === 'enum_values_changed') return false;
|
|
190
|
+
return issue.actual === undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// StorageTable / StorageColumn → flat SqliteTableSpec
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolves codec / `typeRef` / default rendering into a flat
|
|
199
|
+
* `SqliteColumnSpec`. Mirrors Postgres's `toColumnSpec`. Once a column is
|
|
200
|
+
* flattened, downstream Calls and operation factories never see
|
|
201
|
+
* `StorageColumn` again — they deal in pre-rendered SQL fragments.
|
|
202
|
+
*/
|
|
203
|
+
export function toColumnSpec(
|
|
204
|
+
name: string,
|
|
205
|
+
column: StorageColumn,
|
|
206
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance>>,
|
|
207
|
+
inlineAutoincrementPrimaryKey = false,
|
|
208
|
+
): SqliteColumnSpec {
|
|
209
|
+
const typeSql = buildColumnTypeSql(column, storageTypes as Record<string, StorageTypeInstance>);
|
|
210
|
+
const defaultSql = buildColumnDefaultSql(column.default);
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
typeSql,
|
|
214
|
+
defaultSql,
|
|
215
|
+
nullable: column.nullable,
|
|
216
|
+
...(inlineAutoincrementPrimaryKey ? { inlineAutoincrementPrimaryKey: true } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Flattens a `StorageTable` into a `SqliteTableSpec` ready for
|
|
222
|
+
* `CreateTableCall` / `RecreateTableCall`. Sole-column AUTOINCREMENT
|
|
223
|
+
* primary keys are detected here and marked on the column spec so the
|
|
224
|
+
* renderer emits `INTEGER PRIMARY KEY AUTOINCREMENT` inline.
|
|
225
|
+
*/
|
|
226
|
+
export function toTableSpec(
|
|
227
|
+
table: StorageTable,
|
|
228
|
+
storageTypes: Readonly<Record<string, StorageTypeInstance>>,
|
|
229
|
+
): SqliteTableSpec {
|
|
230
|
+
const columns: SqliteColumnSpec[] = Object.entries(table.columns).map(([name, column]) =>
|
|
231
|
+
toColumnSpec(name, column, storageTypes, isInlineAutoincrementPrimaryKey(table, name)),
|
|
232
|
+
);
|
|
233
|
+
const uniques: SqliteUniqueSpec[] = table.uniques.map((u) => ({
|
|
234
|
+
columns: u.columns,
|
|
235
|
+
...(u.name !== undefined ? { name: u.name } : {}),
|
|
236
|
+
}));
|
|
237
|
+
const foreignKeys: SqliteForeignKeySpec[] = table.foreignKeys.map((fk) => ({
|
|
238
|
+
columns: fk.columns,
|
|
239
|
+
references: { table: fk.references.table, columns: fk.references.columns },
|
|
240
|
+
constraint: fk.constraint !== false,
|
|
241
|
+
...(fk.name !== undefined ? { name: fk.name } : {}),
|
|
242
|
+
...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
|
|
243
|
+
...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
|
|
244
|
+
}));
|
|
245
|
+
return {
|
|
246
|
+
columns,
|
|
247
|
+
...(table.primaryKey ? { primaryKey: { columns: table.primaryKey.columns } } : {}),
|
|
248
|
+
uniques,
|
|
249
|
+
foreignKeys,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Issue planner
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
export interface IssuePlannerOptions {
|
|
258
|
+
readonly issues: readonly SchemaIssue[];
|
|
259
|
+
readonly toContract: Contract<SqlStorage>;
|
|
260
|
+
readonly fromContract: Contract<SqlStorage> | null;
|
|
261
|
+
readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
|
|
262
|
+
readonly storageTypes: Readonly<Record<string, StorageTypeInstance>>;
|
|
263
|
+
readonly schema?: SqlSchemaIR;
|
|
264
|
+
readonly policy?: MigrationOperationPolicy;
|
|
265
|
+
readonly frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
266
|
+
readonly strategies?: readonly CallMigrationStrategy[];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface IssuePlannerValue {
|
|
270
|
+
readonly calls: readonly SqliteOpFactoryCall[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const DEFAULT_POLICY: MigrationOperationPolicy = {
|
|
274
|
+
allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
function emptySchemaIR(): SqlSchemaIR {
|
|
278
|
+
return { tables: {}, dependencies: [] };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Issue → Call mapping (per-issue default path)
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
function mapIssueToCall(
|
|
286
|
+
issue: SchemaIssue,
|
|
287
|
+
ctx: StrategyContext,
|
|
288
|
+
): Result<readonly SqliteOpFactoryCall[], SqlPlannerConflict> {
|
|
289
|
+
switch (issue.kind) {
|
|
290
|
+
case 'missing_table': {
|
|
291
|
+
if (!issue.table) {
|
|
292
|
+
return notOk(
|
|
293
|
+
issueConflict('unsupportedOperation', 'Missing table issue has no table name'),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
const contractTable = ctx.toContract.storage.tables[issue.table];
|
|
297
|
+
if (!contractTable) {
|
|
298
|
+
return notOk(
|
|
299
|
+
issueConflict(
|
|
300
|
+
'unsupportedOperation',
|
|
301
|
+
`Table "${issue.table}" reported missing but not found in destination contract`,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const tableSpec = toTableSpec(contractTable, ctx.storageTypes);
|
|
306
|
+
const calls: SqliteOpFactoryCall[] = [new CreateTableCall(issue.table, tableSpec)];
|
|
307
|
+
const declaredIndexColumnKeys = new Set<string>();
|
|
308
|
+
for (const index of contractTable.indexes) {
|
|
309
|
+
const indexName = index.name ?? defaultIndexName(issue.table, index.columns);
|
|
310
|
+
declaredIndexColumnKeys.add(index.columns.join(','));
|
|
311
|
+
calls.push(new CreateIndexCall(issue.table, indexName, index.columns));
|
|
312
|
+
}
|
|
313
|
+
for (const fk of contractTable.foreignKeys) {
|
|
314
|
+
if (fk.index === false) continue;
|
|
315
|
+
if (declaredIndexColumnKeys.has(fk.columns.join(','))) continue;
|
|
316
|
+
const indexName = defaultIndexName(issue.table, fk.columns);
|
|
317
|
+
calls.push(new CreateIndexCall(issue.table, indexName, fk.columns));
|
|
318
|
+
}
|
|
319
|
+
return ok(calls);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'missing_column': {
|
|
323
|
+
if (!issue.table || !issue.column) {
|
|
324
|
+
return notOk(
|
|
325
|
+
issueConflict('unsupportedOperation', 'Missing column issue has no table/column name'),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
|
|
329
|
+
if (!column) {
|
|
330
|
+
return notOk(
|
|
331
|
+
issueConflict(
|
|
332
|
+
'unsupportedOperation',
|
|
333
|
+
`Column "${issue.table}"."${issue.column}" not in destination contract`,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const contractTable = ctx.toContract.storage.tables[issue.table];
|
|
338
|
+
const columnSpec = toColumnSpec(
|
|
339
|
+
issue.column,
|
|
340
|
+
column,
|
|
341
|
+
ctx.storageTypes,
|
|
342
|
+
contractTable ? isInlineAutoincrementPrimaryKey(contractTable, issue.column) : false,
|
|
343
|
+
);
|
|
344
|
+
return ok([new AddColumnCall(issue.table, columnSpec)]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case 'index_mismatch': {
|
|
348
|
+
if (!issue.table) {
|
|
349
|
+
return notOk(issueConflict('indexIncompatible', 'Index issue has no table name'));
|
|
350
|
+
}
|
|
351
|
+
if (!isMissing(issue) || !issue.expected) {
|
|
352
|
+
return notOk(
|
|
353
|
+
issueConflict(
|
|
354
|
+
'indexIncompatible',
|
|
355
|
+
`Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`,
|
|
356
|
+
{ table: issue.table },
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const columns = issue.expected.split(', ');
|
|
361
|
+
const contractTable = ctx.toContract.storage.tables[issue.table];
|
|
362
|
+
if (!contractTable) {
|
|
363
|
+
return notOk(
|
|
364
|
+
issueConflict(
|
|
365
|
+
'unsupportedOperation',
|
|
366
|
+
`Table "${issue.table}" not found in destination contract`,
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
// Use the explicit-index name if one is declared for these columns;
|
|
371
|
+
// otherwise fall back to `defaultIndexName` (which is also what
|
|
372
|
+
// `verifySqlSchema` synthesizes for FK-backing indexes). Whether the
|
|
373
|
+
// missing index originates from `contractTable.indexes` or from an FK
|
|
374
|
+
// with `index: true` doesn't change the emitted DDL.
|
|
375
|
+
const explicitIndex = contractTable.indexes.find(
|
|
376
|
+
(idx) => idx.columns.join(',') === columns.join(','),
|
|
377
|
+
);
|
|
378
|
+
const indexName = explicitIndex?.name ?? defaultIndexName(issue.table, columns);
|
|
379
|
+
return ok([new CreateIndexCall(issue.table, indexName, columns)]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case 'extra_table': {
|
|
383
|
+
if (!issue.table) {
|
|
384
|
+
return notOk(issueConflict('unsupportedOperation', 'Extra table issue has no table name'));
|
|
385
|
+
}
|
|
386
|
+
// Runner-owned control tables must never be dropped.
|
|
387
|
+
if (CONTROL_TABLE_NAMES.has(issue.table)) return ok([]);
|
|
388
|
+
return ok([new DropTableCall(issue.table)]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case 'extra_column': {
|
|
392
|
+
if (!issue.table || !issue.column) {
|
|
393
|
+
return notOk(
|
|
394
|
+
issueConflict('unsupportedOperation', 'Extra column issue has no table/column name'),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
return ok([new DropColumnCall(issue.table, issue.column)]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case 'extra_index': {
|
|
401
|
+
if (!issue.table || !issue.indexOrConstraint) {
|
|
402
|
+
return notOk(
|
|
403
|
+
issueConflict('unsupportedOperation', 'Extra index issue has no table/index name'),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return ok([new DropIndexCall(issue.table, issue.indexOrConstraint)]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// SQLite has no enum types (capability `sql.enums: false`). The verifier
|
|
410
|
+
// should never emit `enum_values_changed` against a SQLite schema, so if
|
|
411
|
+
// we receive one it is a verifier bug — surface it as an explicit
|
|
412
|
+
// conflict rather than silently dropping it.
|
|
413
|
+
case 'enum_values_changed':
|
|
414
|
+
return notOk(
|
|
415
|
+
issueConflict(
|
|
416
|
+
'unsupportedOperation',
|
|
417
|
+
'Received enum_values_changed against a SQLite schema (sql.enums: false) — verifier bug',
|
|
418
|
+
),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Everything below is absorbed by recreateTableStrategy. If it falls
|
|
422
|
+
// through here, policy or context didn't allow the recreate — surface as
|
|
423
|
+
// a conflict.
|
|
424
|
+
case 'type_mismatch':
|
|
425
|
+
case 'nullability_mismatch':
|
|
426
|
+
case 'default_mismatch':
|
|
427
|
+
case 'default_missing':
|
|
428
|
+
case 'extra_default':
|
|
429
|
+
case 'primary_key_mismatch':
|
|
430
|
+
case 'unique_constraint_mismatch':
|
|
431
|
+
case 'foreign_key_mismatch':
|
|
432
|
+
case 'extra_foreign_key':
|
|
433
|
+
case 'extra_unique_constraint':
|
|
434
|
+
case 'extra_primary_key':
|
|
435
|
+
return notOk(issueConflict(conflictKindForIssue(issue), issue.message, issueLocation(issue)));
|
|
436
|
+
|
|
437
|
+
default:
|
|
438
|
+
return notOk(
|
|
439
|
+
issueConflict(
|
|
440
|
+
'unsupportedOperation',
|
|
441
|
+
`Unhandled issue kind: ${(issue as SchemaIssue).kind}`,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================================================
|
|
448
|
+
// Call categorization for final emission order
|
|
449
|
+
// ============================================================================
|
|
450
|
+
|
|
451
|
+
type CallCategory =
|
|
452
|
+
| 'drop-column'
|
|
453
|
+
| 'drop-index'
|
|
454
|
+
| 'drop-table'
|
|
455
|
+
| 'create-table'
|
|
456
|
+
| 'add-column'
|
|
457
|
+
| 'create-index';
|
|
458
|
+
|
|
459
|
+
function classifyCall(call: SqliteOpFactoryCall): CallCategory | null {
|
|
460
|
+
switch (call.factoryName) {
|
|
461
|
+
case 'createTable':
|
|
462
|
+
return 'create-table';
|
|
463
|
+
case 'addColumn':
|
|
464
|
+
return 'add-column';
|
|
465
|
+
case 'createIndex':
|
|
466
|
+
return 'create-index';
|
|
467
|
+
case 'dropColumn':
|
|
468
|
+
return 'drop-column';
|
|
469
|
+
case 'dropIndex':
|
|
470
|
+
return 'drop-index';
|
|
471
|
+
case 'dropTable':
|
|
472
|
+
return 'drop-table';
|
|
473
|
+
// recreateTable goes into the recipe slot; return null for bucketable.
|
|
474
|
+
case 'recreateTable':
|
|
475
|
+
return null;
|
|
476
|
+
default:
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ============================================================================
|
|
482
|
+
// Top-level planIssues
|
|
483
|
+
// ============================================================================
|
|
484
|
+
|
|
485
|
+
export function planIssues(
|
|
486
|
+
options: IssuePlannerOptions,
|
|
487
|
+
): Result<IssuePlannerValue, readonly SqlPlannerConflict[]> {
|
|
488
|
+
const policyProvided = options.policy !== undefined;
|
|
489
|
+
const policy = options.policy ?? DEFAULT_POLICY;
|
|
490
|
+
const schema = options.schema ?? emptySchemaIR();
|
|
491
|
+
const frameworkComponents = options.frameworkComponents ?? [];
|
|
492
|
+
|
|
493
|
+
const context: StrategyContext = {
|
|
494
|
+
toContract: options.toContract,
|
|
495
|
+
fromContract: options.fromContract,
|
|
496
|
+
codecHooks: options.codecHooks,
|
|
497
|
+
storageTypes: options.storageTypes,
|
|
498
|
+
schema,
|
|
499
|
+
policy,
|
|
500
|
+
frameworkComponents,
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const strategies = options.strategies ?? sqlitePlannerStrategies;
|
|
504
|
+
|
|
505
|
+
let remaining = options.issues;
|
|
506
|
+
const recipeCalls: SqliteOpFactoryCall[] = [];
|
|
507
|
+
const bucketableCalls: SqliteOpFactoryCall[] = [];
|
|
508
|
+
|
|
509
|
+
for (const strategy of strategies) {
|
|
510
|
+
const result = strategy(remaining, context);
|
|
511
|
+
if (result.kind === 'match') {
|
|
512
|
+
remaining = result.issues;
|
|
513
|
+
if (result.recipe) {
|
|
514
|
+
recipeCalls.push(...result.calls);
|
|
515
|
+
} else {
|
|
516
|
+
bucketableCalls.push(...result.calls);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const sorted = [...remaining].sort((a, b) => {
|
|
522
|
+
const kindDelta = issueOrder(a) - issueOrder(b);
|
|
523
|
+
if (kindDelta !== 0) return kindDelta;
|
|
524
|
+
const keyA = issueKey(a);
|
|
525
|
+
const keyB = issueKey(b);
|
|
526
|
+
return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const defaultCalls: SqliteOpFactoryCall[] = [];
|
|
530
|
+
const conflicts: SqlPlannerConflict[] = [];
|
|
531
|
+
|
|
532
|
+
for (const issue of sorted) {
|
|
533
|
+
const result = mapIssueToCall(issue, context);
|
|
534
|
+
if (result.ok) {
|
|
535
|
+
defaultCalls.push(...result.value);
|
|
536
|
+
} else {
|
|
537
|
+
conflicts.push(result.failure);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Policy gating for recipe + bucketable. Default-mapped calls for disallowed
|
|
542
|
+
// classes never get here (they're surfaced as per-issue conflicts above).
|
|
543
|
+
const allowed = policy.allowedOperationClasses;
|
|
544
|
+
let gatedRecipe = recipeCalls;
|
|
545
|
+
let gatedBucketable = bucketableCalls;
|
|
546
|
+
let gatedDefault = defaultCalls;
|
|
547
|
+
if (policyProvided) {
|
|
548
|
+
const sink = (acc: SqliteOpFactoryCall[]) => (call: SqliteOpFactoryCall) => {
|
|
549
|
+
if (allowed.includes(call.operationClass)) {
|
|
550
|
+
acc.push(call);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
conflicts.push(conflictForDisallowedCall(call, allowed));
|
|
554
|
+
};
|
|
555
|
+
const gatedRecipeBucket: SqliteOpFactoryCall[] = [];
|
|
556
|
+
const gatedBucketableBucket: SqliteOpFactoryCall[] = [];
|
|
557
|
+
const gatedDefaultBucket: SqliteOpFactoryCall[] = [];
|
|
558
|
+
recipeCalls.forEach(sink(gatedRecipeBucket));
|
|
559
|
+
bucketableCalls.forEach(sink(gatedBucketableBucket));
|
|
560
|
+
defaultCalls.forEach(sink(gatedDefaultBucket));
|
|
561
|
+
gatedRecipe = gatedRecipeBucket;
|
|
562
|
+
gatedBucketable = gatedBucketableBucket;
|
|
563
|
+
gatedDefault = gatedDefaultBucket;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (conflicts.length > 0) {
|
|
567
|
+
return notOk(conflicts);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Final emission order matches the current monolithic planner:
|
|
571
|
+
// create-table → add-column → create-index → recreate → drop-column → drop-index → drop-table
|
|
572
|
+
const combined = [...gatedDefault, ...gatedBucketable];
|
|
573
|
+
const byCategory = (cat: CallCategory) => combined.filter((c) => classifyCall(c) === cat);
|
|
574
|
+
|
|
575
|
+
const calls: SqliteOpFactoryCall[] = [
|
|
576
|
+
...byCategory('create-table'),
|
|
577
|
+
...byCategory('add-column'),
|
|
578
|
+
...byCategory('create-index'),
|
|
579
|
+
...gatedRecipe,
|
|
580
|
+
...byCategory('drop-column'),
|
|
581
|
+
...byCategory('drop-index'),
|
|
582
|
+
...byCategory('drop-table'),
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
return ok({ calls });
|
|
586
|
+
}
|