@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.
Files changed (134) hide show
  1. package/dist/codec-ids-CYwMu3-4.d.mts +13 -0
  2. package/dist/codec-ids-CYwMu3-4.d.mts.map +1 -0
  3. package/dist/codec-ids-CuUxYcd0.mjs +13 -0
  4. package/dist/codec-ids-CuUxYcd0.mjs.map +1 -0
  5. package/dist/codec-ids.d.mts +2 -0
  6. package/dist/codec-ids.mjs +2 -0
  7. package/dist/codec-types-DNauB5UT.d.mts +23 -0
  8. package/dist/codec-types-DNauB5UT.d.mts.map +1 -0
  9. package/dist/codec-types.d.mts +3 -0
  10. package/dist/codec-types.mjs +2 -0
  11. package/dist/codecs-BAlEiSeP.d.mts +126 -0
  12. package/dist/codecs-BAlEiSeP.d.mts.map +1 -0
  13. package/dist/codecs-DVnHtVWW.mjs +220 -0
  14. package/dist/codecs-DVnHtVWW.mjs.map +1 -0
  15. package/dist/codecs.d.mts +2 -0
  16. package/dist/codecs.mjs +13 -0
  17. package/dist/codecs.mjs.map +1 -0
  18. package/dist/control.d.mts +4 -3
  19. package/dist/control.d.mts.map +1 -1
  20. package/dist/control.mjs +428 -5
  21. package/dist/control.mjs.map +1 -1
  22. package/dist/default-normalizer-3Fccw7yw.mjs +69 -0
  23. package/dist/default-normalizer-3Fccw7yw.mjs.map +1 -0
  24. package/dist/default-normalizer.d.mts +7 -0
  25. package/dist/default-normalizer.d.mts.map +1 -0
  26. package/dist/default-normalizer.mjs +2 -0
  27. package/dist/descriptor-meta-CE2Kbn9b.mjs +17 -0
  28. package/dist/descriptor-meta-CE2Kbn9b.mjs.map +1 -0
  29. package/dist/migration.d.mts +85 -0
  30. package/dist/migration.d.mts.map +1 -0
  31. package/dist/migration.mjs +49 -0
  32. package/dist/migration.mjs.map +1 -0
  33. package/dist/native-type-normalizer-BlN5XfD-.mjs +14 -0
  34. package/dist/native-type-normalizer-BlN5XfD-.mjs.map +1 -0
  35. package/dist/native-type-normalizer.d.mts +11 -0
  36. package/dist/native-type-normalizer.d.mts.map +1 -0
  37. package/dist/native-type-normalizer.mjs +2 -0
  38. package/dist/op-factory-call-DRKKURAO.mjs +279 -0
  39. package/dist/op-factory-call-DRKKURAO.mjs.map +1 -0
  40. package/dist/op-factory-call.d.mts +151 -0
  41. package/dist/op-factory-call.d.mts.map +1 -0
  42. package/dist/op-factory-call.mjs +2 -0
  43. package/dist/pack.d.mts +35 -1
  44. package/dist/pack.d.mts.map +1 -1
  45. package/dist/pack.mjs +2 -3
  46. package/dist/planner-CdCU0v1B.mjs +525 -0
  47. package/dist/planner-CdCU0v1B.mjs.map +1 -0
  48. package/dist/planner-produced-sqlite-migration-CI9LdXPr.d.mts +29 -0
  49. package/dist/planner-produced-sqlite-migration-CI9LdXPr.d.mts.map +1 -0
  50. package/dist/planner-produced-sqlite-migration-C_TzWbT0.mjs +110 -0
  51. package/dist/planner-produced-sqlite-migration-C_TzWbT0.mjs.map +1 -0
  52. package/dist/planner-produced-sqlite-migration.d.mts +2 -0
  53. package/dist/planner-produced-sqlite-migration.mjs +2 -0
  54. package/dist/planner-target-details-Bm71XPKb.mjs +15 -0
  55. package/dist/planner-target-details-Bm71XPKb.mjs.map +1 -0
  56. package/dist/planner-target-details-vhvZDWK1.d.mts +12 -0
  57. package/dist/planner-target-details-vhvZDWK1.d.mts.map +1 -0
  58. package/dist/planner-target-details.d.mts +2 -0
  59. package/dist/planner-target-details.mjs +2 -0
  60. package/dist/planner.d.mts +59 -0
  61. package/dist/planner.d.mts.map +1 -0
  62. package/dist/planner.mjs +2 -0
  63. package/dist/render-ops-CSRDT4YL.mjs +8 -0
  64. package/dist/render-ops-CSRDT4YL.mjs.map +1 -0
  65. package/dist/render-ops.d.mts +10 -0
  66. package/dist/render-ops.d.mts.map +1 -0
  67. package/dist/render-ops.mjs +2 -0
  68. package/dist/runtime.d.mts.map +1 -1
  69. package/dist/runtime.mjs +4 -8
  70. package/dist/runtime.mjs.map +1 -1
  71. package/dist/shared-qLsgTOZs.d.mts +69 -0
  72. package/dist/shared-qLsgTOZs.d.mts.map +1 -0
  73. package/dist/sql-utils-DhevMgef.mjs +35 -0
  74. package/dist/sql-utils-DhevMgef.mjs.map +1 -0
  75. package/dist/sql-utils.d.mts +22 -0
  76. package/dist/sql-utils.d.mts.map +1 -0
  77. package/dist/sql-utils.mjs +2 -0
  78. package/dist/sqlite-migration-BBJktVVw.mjs +16 -0
  79. package/dist/sqlite-migration-BBJktVVw.mjs.map +1 -0
  80. package/dist/sqlite-migration-DAb2NEX6.d.mts +17 -0
  81. package/dist/sqlite-migration-DAb2NEX6.d.mts.map +1 -0
  82. package/dist/statement-builders-Dne-LkAV.mjs +158 -0
  83. package/dist/statement-builders-Dne-LkAV.mjs.map +1 -0
  84. package/dist/statement-builders.d.mts +68 -0
  85. package/dist/statement-builders.d.mts.map +1 -0
  86. package/dist/statement-builders.mjs +2 -0
  87. package/dist/tables-D84zfPZI.mjs +403 -0
  88. package/dist/tables-D84zfPZI.mjs.map +1 -0
  89. package/package.json +33 -11
  90. package/src/core/authoring.ts +9 -0
  91. package/src/core/codec-helpers.ts +11 -0
  92. package/src/core/codec-ids.ts +13 -0
  93. package/src/core/codecs.ts +337 -0
  94. package/src/core/control-target.ts +54 -11
  95. package/src/core/default-normalizer.ts +92 -0
  96. package/src/core/descriptor-meta.ts +5 -1
  97. package/src/core/migrations/issue-planner.ts +586 -0
  98. package/src/core/migrations/op-factory-call.ts +369 -0
  99. package/src/core/migrations/operations/columns.ts +62 -0
  100. package/src/core/migrations/operations/data-transform.ts +51 -0
  101. package/src/core/migrations/operations/indexes.ts +52 -0
  102. package/src/core/migrations/operations/raw.ts +12 -0
  103. package/src/core/migrations/operations/shared.ts +120 -0
  104. package/src/core/migrations/operations/tables.ts +388 -0
  105. package/src/core/migrations/planner-ddl-builders.ts +142 -0
  106. package/src/core/migrations/planner-produced-sqlite-migration.ts +70 -0
  107. package/src/core/migrations/planner-strategies.ts +231 -0
  108. package/src/core/migrations/planner-target-details.ts +33 -0
  109. package/src/core/migrations/planner.ts +183 -0
  110. package/src/core/migrations/render-ops.ts +15 -0
  111. package/src/core/migrations/render-typescript.ts +91 -0
  112. package/src/core/migrations/runner.ts +724 -0
  113. package/src/core/migrations/sqlite-migration.ts +13 -0
  114. package/src/core/migrations/statement-builders.ts +212 -0
  115. package/src/core/native-type-normalizer.ts +9 -0
  116. package/src/core/registry.ts +11 -0
  117. package/src/core/runtime-target.ts +1 -3
  118. package/src/core/sql-utils.ts +47 -0
  119. package/src/exports/codec-ids.ts +13 -0
  120. package/src/exports/codec-types.ts +43 -0
  121. package/src/exports/codecs.ts +20 -0
  122. package/src/exports/control.ts +1 -0
  123. package/src/exports/default-normalizer.ts +1 -0
  124. package/src/exports/migration.ts +24 -0
  125. package/src/exports/native-type-normalizer.ts +1 -0
  126. package/src/exports/op-factory-call.ts +12 -0
  127. package/src/exports/planner-produced-sqlite-migration.ts +4 -0
  128. package/src/exports/planner-target-details.ts +2 -0
  129. package/src/exports/planner.ts +2 -0
  130. package/src/exports/render-ops.ts +1 -0
  131. package/src/exports/sql-utils.ts +1 -0
  132. package/src/exports/statement-builders.ts +12 -0
  133. package/dist/descriptor-meta-DbuuziYA.mjs +0 -14
  134. 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
+ }