@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,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite migration strategies.
|
|
3
|
+
*
|
|
4
|
+
* Each strategy examines the issue list, consumes issues it handles, and
|
|
5
|
+
* returns the `SqliteOpFactoryCall[]` to address them. The issue planner
|
|
6
|
+
* runs each strategy in order and routes whatever's left through
|
|
7
|
+
* `mapIssueToCall`.
|
|
8
|
+
*
|
|
9
|
+
* SQLite has no enums, no data-safe backfill, and no component-declared
|
|
10
|
+
* database dependencies. The only recipe that needs strategy-level
|
|
11
|
+
* multi-issue consumption is `recreateTable` (added in a later phase), which
|
|
12
|
+
* absorbs type/nullability/default/constraint mismatches for a given table
|
|
13
|
+
* into a single recreate operation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
17
|
+
import type {
|
|
18
|
+
CodecControlHooks,
|
|
19
|
+
MigrationOperationClass,
|
|
20
|
+
MigrationOperationPolicy,
|
|
21
|
+
} from '@prisma-next/family-sql/control';
|
|
22
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
23
|
+
import type { SchemaIssue } from '@prisma-next/framework-components/control';
|
|
24
|
+
import type { SqlStorage, StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
25
|
+
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
26
|
+
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
27
|
+
import { toTableSpec } from './issue-planner';
|
|
28
|
+
import { DataTransformCall, RecreateTableCall, type SqliteOpFactoryCall } from './op-factory-call';
|
|
29
|
+
import type { SqliteIndexSpec } from './operations/shared';
|
|
30
|
+
import { buildRecreatePostchecks, buildRecreateSummary } from './operations/tables';
|
|
31
|
+
|
|
32
|
+
export interface StrategyContext {
|
|
33
|
+
readonly toContract: Contract<SqlStorage>;
|
|
34
|
+
readonly fromContract: Contract<SqlStorage> | null;
|
|
35
|
+
readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
|
|
36
|
+
readonly storageTypes: Readonly<Record<string, StorageTypeInstance>>;
|
|
37
|
+
readonly schema: SqlSchemaIR;
|
|
38
|
+
readonly policy: MigrationOperationPolicy;
|
|
39
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CallMigrationStrategy = (
|
|
43
|
+
issues: readonly SchemaIssue[],
|
|
44
|
+
context: StrategyContext,
|
|
45
|
+
) =>
|
|
46
|
+
| {
|
|
47
|
+
kind: 'match';
|
|
48
|
+
issues: readonly SchemaIssue[];
|
|
49
|
+
calls: readonly SqliteOpFactoryCall[];
|
|
50
|
+
recipe?: boolean;
|
|
51
|
+
}
|
|
52
|
+
| { kind: 'no_match' };
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Recreate-table strategy
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
const WIDENING_ISSUE_KINDS = new Set<SchemaIssue['kind']>(['default_mismatch', 'default_missing']);
|
|
59
|
+
|
|
60
|
+
const DESTRUCTIVE_ISSUE_KINDS = new Set<SchemaIssue['kind']>([
|
|
61
|
+
'extra_default',
|
|
62
|
+
'type_mismatch',
|
|
63
|
+
'primary_key_mismatch',
|
|
64
|
+
'foreign_key_mismatch',
|
|
65
|
+
'unique_constraint_mismatch',
|
|
66
|
+
'extra_foreign_key',
|
|
67
|
+
'extra_unique_constraint',
|
|
68
|
+
'extra_primary_key',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function classifyIssue(issue: SchemaIssue): 'widening' | 'destructive' | null {
|
|
72
|
+
if (issue.kind === 'enum_values_changed') return null;
|
|
73
|
+
if (!issue.table) return null;
|
|
74
|
+
if (issue.kind === 'nullability_mismatch') {
|
|
75
|
+
// Relaxing (NOT NULL → nullable) is widening; tightening is destructive.
|
|
76
|
+
return issue.expected === 'true' ? 'widening' : 'destructive';
|
|
77
|
+
}
|
|
78
|
+
if (WIDENING_ISSUE_KINDS.has(issue.kind)) return 'widening';
|
|
79
|
+
if (DESTRUCTIVE_ISSUE_KINDS.has(issue.kind)) return 'destructive';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Groups recreate-eligible issues by table, decides per-table operation class
|
|
85
|
+
* (destructive wins over widening), and emits one `RecreateTableCall` per
|
|
86
|
+
* table. Returns unchanged-or-smaller issue list — issues the strategy
|
|
87
|
+
* consumed are removed so `mapIssueToCall` doesn't double-handle them.
|
|
88
|
+
*/
|
|
89
|
+
export const recreateTableStrategy: CallMigrationStrategy = (issues, ctx) => {
|
|
90
|
+
const byTable = new Map<string, { issues: SchemaIssue[]; hasDestructive: boolean }>();
|
|
91
|
+
const consumed = new Set<SchemaIssue>();
|
|
92
|
+
|
|
93
|
+
for (const issue of issues) {
|
|
94
|
+
const cls = classifyIssue(issue);
|
|
95
|
+
if (!cls) continue;
|
|
96
|
+
if (issue.kind === 'enum_values_changed') continue;
|
|
97
|
+
if (!issue.table) continue;
|
|
98
|
+
const table = issue.table;
|
|
99
|
+
const entry = byTable.get(table);
|
|
100
|
+
if (entry) {
|
|
101
|
+
entry.issues.push(issue);
|
|
102
|
+
if (cls === 'destructive') entry.hasDestructive = true;
|
|
103
|
+
} else {
|
|
104
|
+
byTable.set(table, { issues: [issue], hasDestructive: cls === 'destructive' });
|
|
105
|
+
}
|
|
106
|
+
consumed.add(issue);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (byTable.size === 0) return { kind: 'no_match' };
|
|
110
|
+
|
|
111
|
+
const calls: SqliteOpFactoryCall[] = [];
|
|
112
|
+
for (const [tableName, entry] of byTable) {
|
|
113
|
+
const contractTable = ctx.toContract.storage.tables[tableName];
|
|
114
|
+
const schemaTable = ctx.schema.tables[tableName];
|
|
115
|
+
if (!contractTable || !schemaTable) continue;
|
|
116
|
+
const operationClass: MigrationOperationClass = entry.hasDestructive
|
|
117
|
+
? 'destructive'
|
|
118
|
+
: 'widening';
|
|
119
|
+
|
|
120
|
+
// Flatten the contract table to a self-contained spec — the Call holds
|
|
121
|
+
// pre-rendered SQL fragments only, no `StorageColumn` or `storageTypes`.
|
|
122
|
+
const tableSpec = toTableSpec(contractTable, ctx.storageTypes);
|
|
123
|
+
|
|
124
|
+
const seenIndexColumnKeys = new Set<string>();
|
|
125
|
+
const indexes: SqliteIndexSpec[] = [];
|
|
126
|
+
for (const idx of contractTable.indexes) {
|
|
127
|
+
const key = idx.columns.join(',');
|
|
128
|
+
if (seenIndexColumnKeys.has(key)) continue;
|
|
129
|
+
seenIndexColumnKeys.add(key);
|
|
130
|
+
indexes.push({
|
|
131
|
+
name: idx.name ?? defaultIndexName(tableName, idx.columns),
|
|
132
|
+
columns: idx.columns,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
for (const fk of contractTable.foreignKeys) {
|
|
136
|
+
if (fk.index === false) continue;
|
|
137
|
+
const key = fk.columns.join(',');
|
|
138
|
+
if (seenIndexColumnKeys.has(key)) continue;
|
|
139
|
+
seenIndexColumnKeys.add(key);
|
|
140
|
+
indexes.push({
|
|
141
|
+
name: defaultIndexName(tableName, fk.columns),
|
|
142
|
+
columns: fk.columns,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
calls.push(
|
|
147
|
+
new RecreateTableCall({
|
|
148
|
+
tableName,
|
|
149
|
+
contractTable: tableSpec,
|
|
150
|
+
schemaColumnNames: Object.keys(schemaTable.columns),
|
|
151
|
+
indexes,
|
|
152
|
+
summary: buildRecreateSummary(tableName, entry.issues),
|
|
153
|
+
postchecks: buildRecreatePostchecks(tableName, entry.issues, tableSpec),
|
|
154
|
+
operationClass,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
kind: 'match',
|
|
161
|
+
issues: issues.filter((i) => !consumed.has(i)),
|
|
162
|
+
calls,
|
|
163
|
+
recipe: true,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Nullability-tightening backfill strategy
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* When the policy allows `'data'` and the contract tightens one or more
|
|
173
|
+
* columns from nullable to NOT NULL, emit a `DataTransformCall` stub per
|
|
174
|
+
* tightened column. The user fills the backfill `UPDATE` in the rendered
|
|
175
|
+
* `migration.ts` before the subsequent `RecreateTableCall` copies data into
|
|
176
|
+
* the tightened schema (whose `INSERT INTO temp SELECT … FROM old` would
|
|
177
|
+
* otherwise fail at runtime if any `NULL`s remain).
|
|
178
|
+
*
|
|
179
|
+
* Does NOT consume the tightening issue — `recreateTableStrategy` still
|
|
180
|
+
* needs it to produce the actual recreate that enforces the NOT NULL at
|
|
181
|
+
* the schema level. The backfill op and the recreate op end up in the
|
|
182
|
+
* recipe slot in strategy order (backfill first, recreate second), which
|
|
183
|
+
* matches the required execution order.
|
|
184
|
+
*
|
|
185
|
+
* Mirrors Postgres's `nullableTighteningCallStrategy` / `'data'`-class
|
|
186
|
+
* gating. When `'data'` is not in the policy (the default `db update` /
|
|
187
|
+
* `db init` path), the strategy short-circuits and the recreate alone
|
|
188
|
+
* runs with its current destructive-class gating — preserving today's
|
|
189
|
+
* behavior where a tightening blows up at runtime if NULLs are present.
|
|
190
|
+
*/
|
|
191
|
+
export const nullabilityTighteningBackfillStrategy: CallMigrationStrategy = (issues, ctx) => {
|
|
192
|
+
if (!ctx.policy.allowedOperationClasses.includes('data')) {
|
|
193
|
+
return { kind: 'no_match' };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const calls: SqliteOpFactoryCall[] = [];
|
|
197
|
+
for (const issue of issues) {
|
|
198
|
+
if (issue.kind !== 'nullability_mismatch') continue;
|
|
199
|
+
if (!issue.table || !issue.column) continue;
|
|
200
|
+
// Tightening only: `expected === 'true'` means the contract wants the
|
|
201
|
+
// column nullable (relaxing from NOT NULL → nullable), which is safe and
|
|
202
|
+
// needs no backfill.
|
|
203
|
+
if (issue.expected === 'true') continue;
|
|
204
|
+
|
|
205
|
+
const column = ctx.toContract.storage.tables[issue.table]?.columns[issue.column];
|
|
206
|
+
if (!column || column.nullable === true) continue;
|
|
207
|
+
|
|
208
|
+
calls.push(
|
|
209
|
+
new DataTransformCall(
|
|
210
|
+
`data_migration.backfill-${issue.table}-${issue.column}`,
|
|
211
|
+
`Backfill NULLs in "${issue.table}"."${issue.column}" before NOT NULL tightening`,
|
|
212
|
+
issue.table,
|
|
213
|
+
issue.column,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (calls.length === 0) return { kind: 'no_match' };
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
kind: 'match',
|
|
222
|
+
issues,
|
|
223
|
+
calls,
|
|
224
|
+
recipe: true,
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const sqlitePlannerStrategies: readonly CallMigrationStrategy[] = [
|
|
229
|
+
nullabilityTighteningBackfillStrategy,
|
|
230
|
+
recreateTableStrategy,
|
|
231
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
|
+
|
|
3
|
+
export type OperationClass = 'table' | 'column' | 'primaryKey' | 'unique' | 'index' | 'foreignKey';
|
|
4
|
+
|
|
5
|
+
// SQLite's default (and only) schema name; keeps `SqlitePlanTargetDetails`
|
|
6
|
+
// conformant with `SqlPlanTargetDetails`, which mandates a `schema` field.
|
|
7
|
+
const DEFAULT_SCHEMA = 'main';
|
|
8
|
+
|
|
9
|
+
export interface SqlitePlanTargetDetails {
|
|
10
|
+
readonly schema: string;
|
|
11
|
+
readonly objectType: OperationClass;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly table?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PlanningMode {
|
|
17
|
+
readonly includeExtraObjects: boolean;
|
|
18
|
+
readonly allowWidening: boolean;
|
|
19
|
+
readonly allowDestructive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildTargetDetails(
|
|
23
|
+
objectType: OperationClass,
|
|
24
|
+
name: string,
|
|
25
|
+
table?: string,
|
|
26
|
+
): SqlitePlanTargetDetails {
|
|
27
|
+
return {
|
|
28
|
+
schema: DEFAULT_SCHEMA,
|
|
29
|
+
objectType,
|
|
30
|
+
name,
|
|
31
|
+
...ifDefined('table', table),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
MigrationOperationPolicy,
|
|
4
|
+
SqlMigrationPlanner,
|
|
5
|
+
SqlMigrationPlannerPlanOptions,
|
|
6
|
+
SqlPlannerFailureResult,
|
|
7
|
+
} from '@prisma-next/family-sql/control';
|
|
8
|
+
import {
|
|
9
|
+
extractCodecControlHooks,
|
|
10
|
+
planFieldEventOperations,
|
|
11
|
+
plannerFailure,
|
|
12
|
+
} from '@prisma-next/family-sql/control';
|
|
13
|
+
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
14
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
15
|
+
import type {
|
|
16
|
+
MigrationPlanner,
|
|
17
|
+
MigrationScaffoldContext,
|
|
18
|
+
SchemaIssue,
|
|
19
|
+
} from '@prisma-next/framework-components/control';
|
|
20
|
+
import { parseSqliteDefault } from '../default-normalizer';
|
|
21
|
+
import { normalizeSqliteNativeType } from '../native-type-normalizer';
|
|
22
|
+
import { planIssues } from './issue-planner';
|
|
23
|
+
import {
|
|
24
|
+
type SqliteMigrationDestinationInfo,
|
|
25
|
+
TypeScriptRenderableSqliteMigration,
|
|
26
|
+
} from './planner-produced-sqlite-migration';
|
|
27
|
+
import { sqlitePlannerStrategies } from './planner-strategies';
|
|
28
|
+
import type { SqlitePlanTargetDetails } from './planner-target-details';
|
|
29
|
+
|
|
30
|
+
export function createSqliteMigrationPlanner(): SqliteMigrationPlanner {
|
|
31
|
+
return new SqliteMigrationPlanner();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type SqlitePlanResult =
|
|
35
|
+
| { readonly kind: 'success'; readonly plan: TypeScriptRenderableSqliteMigration }
|
|
36
|
+
| SqlPlannerFailureResult;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SQLite migration planner — a thin wrapper over `planIssues`.
|
|
40
|
+
*
|
|
41
|
+
* `plan()` verifies the live schema against the target contract (producing
|
|
42
|
+
* `SchemaIssue[]`) and delegates to `planIssues` with the registered
|
|
43
|
+
* strategies. Strategies absorb groups of related issues into composite
|
|
44
|
+
* recipes (e.g. recreating a table to apply type/nullability/default/
|
|
45
|
+
* constraint changes at once); anything not absorbed by a strategy flows
|
|
46
|
+
* through `mapIssueToCall` in the issue planner as a one-off call.
|
|
47
|
+
*
|
|
48
|
+
* FK-backing indexes are surfaced by `verifySqlSchema`'s index expansion
|
|
49
|
+
* (see `verify-sql-schema.ts:459-469`), so `mapIssueToCall` handles them
|
|
50
|
+
* uniformly alongside user-declared indexes.
|
|
51
|
+
*/
|
|
52
|
+
export class SqliteMigrationPlanner
|
|
53
|
+
implements SqlMigrationPlanner<SqlitePlanTargetDetails>, MigrationPlanner<'sql', 'sqlite'>
|
|
54
|
+
{
|
|
55
|
+
plan(options: {
|
|
56
|
+
readonly contract: unknown;
|
|
57
|
+
readonly schema: unknown;
|
|
58
|
+
readonly policy: MigrationOperationPolicy;
|
|
59
|
+
/**
|
|
60
|
+
* The "from" contract (state the planner assumes the database starts at),
|
|
61
|
+
* or `null` for reconciliation flows.
|
|
62
|
+
*
|
|
63
|
+
* Typed as the framework `Contract | null` to satisfy the
|
|
64
|
+
* `MigrationPlanner` interface contract; `planSql` narrows to the SQL
|
|
65
|
+
* shape via `SqlMigrationPlannerPlanOptions`. Used to populate
|
|
66
|
+
* `describe().from` on the produced plan as
|
|
67
|
+
* `fromContract?.storage.storageHash ?? null`.
|
|
68
|
+
*/
|
|
69
|
+
readonly fromContract: Contract | null;
|
|
70
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
71
|
+
/**
|
|
72
|
+
* Contract space this plan applies to. Stamped onto the produced
|
|
73
|
+
* {@link TypeScriptRenderableSqliteMigration.spaceId} so the runner keys
|
|
74
|
+
* the marker row by the right space.
|
|
75
|
+
*/
|
|
76
|
+
readonly spaceId: string;
|
|
77
|
+
}): SqlitePlanResult {
|
|
78
|
+
return this.planSql(options as SqlMigrationPlannerPlanOptions);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
emptyMigration(
|
|
82
|
+
context: MigrationScaffoldContext,
|
|
83
|
+
spaceId: string,
|
|
84
|
+
): TypeScriptRenderableSqliteMigration {
|
|
85
|
+
return new TypeScriptRenderableSqliteMigration(
|
|
86
|
+
[],
|
|
87
|
+
{
|
|
88
|
+
from: context.fromHash,
|
|
89
|
+
to: context.toHash,
|
|
90
|
+
},
|
|
91
|
+
spaceId,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private planSql(options: SqlMigrationPlannerPlanOptions): SqlitePlanResult {
|
|
96
|
+
const policyResult = this.ensureAdditivePolicy(options.policy);
|
|
97
|
+
if (policyResult) return policyResult;
|
|
98
|
+
|
|
99
|
+
const schemaIssues = this.collectSchemaIssues(options);
|
|
100
|
+
const codecHooks = extractCodecControlHooks(options.frameworkComponents);
|
|
101
|
+
const storageTypes = options.contract.storage.types ?? {};
|
|
102
|
+
|
|
103
|
+
const result = planIssues({
|
|
104
|
+
issues: schemaIssues,
|
|
105
|
+
toContract: options.contract,
|
|
106
|
+
fromContract: options.fromContract,
|
|
107
|
+
codecHooks,
|
|
108
|
+
storageTypes,
|
|
109
|
+
schema: options.schema,
|
|
110
|
+
policy: options.policy,
|
|
111
|
+
frameworkComponents: options.frameworkComponents,
|
|
112
|
+
strategies: sqlitePlannerStrategies,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!result.ok) {
|
|
116
|
+
return plannerFailure(result.failure);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Codec lifecycle hook (T2.2): inline `onFieldEvent`-emitted ops after
|
|
120
|
+
// structural DDL. Sub-spec § 5 fixes the ordering as
|
|
121
|
+
// `structural → added → dropped → altered`, with within-group sorting by
|
|
122
|
+
// `(tableName, fieldName)` deterministic for byte-stable re-emits.
|
|
123
|
+
// Hook fires only at the application emitter — extension-space planning
|
|
124
|
+
// (M2 R2) never reaches this helper.
|
|
125
|
+
const fieldEventOps = planFieldEventOperations({
|
|
126
|
+
priorContract: options.fromContract,
|
|
127
|
+
newContract: options.contract,
|
|
128
|
+
codecHooks,
|
|
129
|
+
});
|
|
130
|
+
// Codec-emitted calls already conform to `OpFactoryCall` — render +
|
|
131
|
+
// toOp + importRequirements ride directly through the same emit path
|
|
132
|
+
// as structural ops, no `RawSqlCall` wrap.
|
|
133
|
+
const calls = [...result.value.calls, ...fieldEventOps];
|
|
134
|
+
|
|
135
|
+
const destination: SqliteMigrationDestinationInfo = {
|
|
136
|
+
storageHash: options.contract.storage.storageHash,
|
|
137
|
+
...(options.contract.profileHash !== undefined
|
|
138
|
+
? { profileHash: options.contract.profileHash }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
kind: 'success' as const,
|
|
144
|
+
plan: new TypeScriptRenderableSqliteMigration(
|
|
145
|
+
calls,
|
|
146
|
+
{
|
|
147
|
+
from: options.fromContract?.storage.storageHash ?? null,
|
|
148
|
+
to: options.contract.storage.storageHash,
|
|
149
|
+
},
|
|
150
|
+
options.spaceId,
|
|
151
|
+
destination,
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private ensureAdditivePolicy(policy: MigrationOperationPolicy): SqlPlannerFailureResult | null {
|
|
157
|
+
if (!policy.allowedOperationClasses.includes('additive')) {
|
|
158
|
+
return plannerFailure([
|
|
159
|
+
{
|
|
160
|
+
kind: 'unsupportedOperation',
|
|
161
|
+
summary: 'Migration planner requires additive operations be allowed',
|
|
162
|
+
why: 'The planner requires the "additive" operation class to be allowed in the policy.',
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private collectSchemaIssues(options: SqlMigrationPlannerPlanOptions): readonly SchemaIssue[] {
|
|
170
|
+
const allowed = options.policy.allowedOperationClasses;
|
|
171
|
+
const strict = allowed.includes('widening') || allowed.includes('destructive');
|
|
172
|
+
const verifyResult = verifySqlSchema({
|
|
173
|
+
contract: options.contract,
|
|
174
|
+
schema: options.schema,
|
|
175
|
+
strict,
|
|
176
|
+
typeMetadataRegistry: new Map(),
|
|
177
|
+
frameworkComponents: options.frameworkComponents,
|
|
178
|
+
normalizeDefault: parseSqliteDefault,
|
|
179
|
+
normalizeNativeType: normalizeSqliteNativeType,
|
|
180
|
+
});
|
|
181
|
+
return verifyResult.schema.issues;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control';
|
|
2
|
+
import type { OpFactoryCall } from '@prisma-next/framework-components/control';
|
|
3
|
+
import type { SqlitePlanTargetDetails } from './planner-target-details';
|
|
4
|
+
|
|
5
|
+
type Op = SqlMigrationPlanOperation<SqlitePlanTargetDetails>;
|
|
6
|
+
|
|
7
|
+
export function renderOps(calls: readonly OpFactoryCall[]): Op[] {
|
|
8
|
+
// Each call's `toOp()` is typed as the framework `MigrationPlanOperation`;
|
|
9
|
+
// every concrete Call class on the sqlite planner path produces an op
|
|
10
|
+
// whose `target.details` is `SqlitePlanTargetDetails`-shaped (or whose
|
|
11
|
+
// `target.details` is absent, which is structurally compatible). The
|
|
12
|
+
// narrowing cast happens at this single integration boundary instead of
|
|
13
|
+
// poisoning every caller's type.
|
|
14
|
+
return calls.map((c) => c.toOp() as Op);
|
|
15
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polymorphic TypeScript emitter for the SQLite migration IR. Mirrors the
|
|
3
|
+
* Postgres `render-typescript.ts` — different base-class + factory module
|
|
4
|
+
* specifier, same overall shape.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { OpFactoryCall } from '@prisma-next/framework-components/control';
|
|
8
|
+
import { detectScaffoldRuntime, shebangLineFor } from '@prisma-next/migration-tools/migration-ts';
|
|
9
|
+
import { type ImportRequirement, jsonToTsSource, renderImports } from '@prisma-next/ts-render';
|
|
10
|
+
|
|
11
|
+
export interface RenderMigrationMeta {
|
|
12
|
+
readonly from: string | null;
|
|
13
|
+
readonly to: string;
|
|
14
|
+
readonly labels?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Always-present base imports for the rendered scaffold. Both come from
|
|
19
|
+
* `@prisma-next/target-sqlite/migration` so an authored SQLite
|
|
20
|
+
* `migration.ts` only needs a single dependency for its base class and
|
|
21
|
+
* its CLI entrypoint. Mirrors Postgres's `BASE_IMPORTS`.
|
|
22
|
+
*
|
|
23
|
+
* - `Migration` — the target-owned re-export fixes the `SqlMigration`
|
|
24
|
+
* generic to `SqlitePlanTargetDetails` and the abstract `targetId` to
|
|
25
|
+
* `'sqlite'`.
|
|
26
|
+
* - `MigrationCLI` — the migration-file CLI entrypoint, re-exported from
|
|
27
|
+
* `@prisma-next/cli/migration-cli`. Loads `prisma-next.config.ts`,
|
|
28
|
+
* assembles a `ControlStack`, and instantiates the migration class.
|
|
29
|
+
*/
|
|
30
|
+
const BASE_IMPORTS: readonly ImportRequirement[] = [
|
|
31
|
+
{ moduleSpecifier: '@prisma-next/target-sqlite/migration', symbol: 'Migration' },
|
|
32
|
+
{ moduleSpecifier: '@prisma-next/target-sqlite/migration', symbol: 'MigrationCLI' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function renderCallsToTypeScript(
|
|
36
|
+
calls: ReadonlyArray<OpFactoryCall>,
|
|
37
|
+
meta: RenderMigrationMeta,
|
|
38
|
+
): string {
|
|
39
|
+
const imports = buildImports(calls);
|
|
40
|
+
const operationsBody = calls.map((c) => c.renderTypeScript()).join(',\n');
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
shebangLineFor(detectScaffoldRuntime()),
|
|
44
|
+
imports,
|
|
45
|
+
'',
|
|
46
|
+
'export default class M extends Migration {',
|
|
47
|
+
buildDescribeMethod(meta),
|
|
48
|
+
' override get operations() {',
|
|
49
|
+
' return [',
|
|
50
|
+
indent(operationsBody, 6),
|
|
51
|
+
' ];',
|
|
52
|
+
' }',
|
|
53
|
+
'}',
|
|
54
|
+
'',
|
|
55
|
+
'MigrationCLI.run(import.meta.url, M);',
|
|
56
|
+
'',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildImports(calls: ReadonlyArray<OpFactoryCall>): string {
|
|
61
|
+
const requirements: ImportRequirement[] = [...BASE_IMPORTS];
|
|
62
|
+
for (const call of calls) {
|
|
63
|
+
for (const req of call.importRequirements()) {
|
|
64
|
+
requirements.push(req);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return renderImports(requirements);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildDescribeMethod(meta: RenderMigrationMeta): string {
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
lines.push(' override describe() {');
|
|
73
|
+
lines.push(' return {');
|
|
74
|
+
lines.push(` from: ${JSON.stringify(meta.from)},`);
|
|
75
|
+
lines.push(` to: ${JSON.stringify(meta.to)},`);
|
|
76
|
+
if (meta.labels && meta.labels.length > 0) {
|
|
77
|
+
lines.push(` labels: ${jsonToTsSource(meta.labels)},`);
|
|
78
|
+
}
|
|
79
|
+
lines.push(' };');
|
|
80
|
+
lines.push(' }');
|
|
81
|
+
lines.push('');
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function indent(text: string, spaces: number): string {
|
|
86
|
+
const pad = ' '.repeat(spaces);
|
|
87
|
+
return text
|
|
88
|
+
.split('\n')
|
|
89
|
+
.map((line) => (line.trim() ? `${pad}${line}` : line))
|
|
90
|
+
.join('\n');
|
|
91
|
+
}
|