@prisma-next/target-sqlite 0.14.0 → 0.15.0-dev.10

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 (82) hide show
  1. package/dist/codecs-BGeJavlQ.d.mts.map +1 -1
  2. package/dist/codecs.mjs +220 -2
  3. package/dist/codecs.mjs.map +1 -1
  4. package/dist/contract-free.mjs +1 -1
  5. package/dist/{control-tables-7KwMyJ6i.mjs → control-tables-D_o8dKRB.mjs} +2 -2
  6. package/dist/{control-tables-7KwMyJ6i.mjs.map → control-tables-D_o8dKRB.mjs.map} +1 -1
  7. package/dist/control-tables.mjs +1 -1
  8. package/dist/control.d.mts +3 -4
  9. package/dist/control.d.mts.map +1 -1
  10. package/dist/control.mjs +17 -28
  11. package/dist/control.mjs.map +1 -1
  12. package/dist/{descriptor-meta-DxmEeTJ-.mjs → descriptor-meta-D1fap-5t.mjs} +4 -11
  13. package/dist/descriptor-meta-D1fap-5t.mjs.map +1 -0
  14. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs +12 -0
  15. package/dist/descriptor-meta-runtime-BkXK3OjD.mjs.map +1 -0
  16. package/dist/migration.d.mts +2 -2
  17. package/dist/migration.mjs +2 -2
  18. package/dist/native-type-normalizer.mjs +13 -1
  19. package/dist/native-type-normalizer.mjs.map +1 -0
  20. package/dist/{op-factory-call-DmdfD1yd.mjs → op-factory-call-BlMqc5G_.mjs} +134 -117
  21. package/dist/op-factory-call-BlMqc5G_.mjs.map +1 -0
  22. package/dist/op-factory-call.d.mts +2 -2
  23. package/dist/op-factory-call.d.mts.map +1 -1
  24. package/dist/op-factory-call.mjs +1 -1
  25. package/dist/pack.mjs +1 -1
  26. package/dist/planner-CxbLEZHF.mjs +815 -0
  27. package/dist/planner-CxbLEZHF.mjs.map +1 -0
  28. package/dist/{planner-produced-sqlite-migration-0xPEm3R1.mjs → planner-produced-sqlite-migration-D5415zDp.mjs} +46 -17
  29. package/dist/planner-produced-sqlite-migration-D5415zDp.mjs.map +1 -0
  30. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts → planner-produced-sqlite-migration-DKopa0rd.d.mts} +3 -3
  31. package/dist/{planner-produced-sqlite-migration-CpgsY-M9.d.mts.map → planner-produced-sqlite-migration-DKopa0rd.d.mts.map} +1 -1
  32. package/dist/planner-produced-sqlite-migration.d.mts +1 -1
  33. package/dist/planner-produced-sqlite-migration.mjs +1 -1
  34. package/dist/planner.d.mts +47 -11
  35. package/dist/planner.d.mts.map +1 -1
  36. package/dist/planner.mjs +1 -1
  37. package/dist/runtime.d.mts +6 -4
  38. package/dist/runtime.d.mts.map +1 -1
  39. package/dist/runtime.mjs +3 -3
  40. package/dist/{shared-Dhc8mLK1.d.mts → shared-CaiRyjA0.d.mts} +2 -3
  41. package/dist/{shared-Dhc8mLK1.d.mts.map → shared-CaiRyjA0.d.mts.map} +1 -1
  42. package/dist/{sqlite-contract-serializer--iaDgC8e.mjs → sqlite-contract-view-B8qeCczc.mjs} +35 -10
  43. package/dist/sqlite-contract-view-B8qeCczc.mjs.map +1 -0
  44. package/dist/sqlite-contract-view-J32K82Gi.d.mts +29 -0
  45. package/dist/sqlite-contract-view-J32K82Gi.d.mts.map +1 -0
  46. package/dist/{sqlite-migration-DVfhQwN_.d.mts → sqlite-migration-Dzz9p0Gw.d.mts} +35 -6
  47. package/dist/sqlite-migration-Dzz9p0Gw.d.mts.map +1 -0
  48. package/dist/sqlite-migration-WdRHavfF.mjs +123 -0
  49. package/dist/sqlite-migration-WdRHavfF.mjs.map +1 -0
  50. package/package.json +20 -20
  51. package/src/core/codecs.ts +10 -3
  52. package/src/core/control-target.ts +13 -18
  53. package/src/core/errors.ts +14 -12
  54. package/src/core/migrations/column-ddl-rendering.ts +177 -0
  55. package/src/core/migrations/diff-database-schema.ts +230 -0
  56. package/src/core/migrations/issue-planner.ts +333 -498
  57. package/src/core/migrations/op-factory-call.ts +5 -0
  58. package/src/core/migrations/operations/shared.ts +3 -5
  59. package/src/core/migrations/operations/tables.ts +108 -68
  60. package/src/core/migrations/planner-strategies.ts +119 -137
  61. package/src/core/migrations/planner.ts +118 -30
  62. package/src/core/migrations/render-typescript.ts +42 -15
  63. package/src/core/migrations/runner.ts +4 -10
  64. package/src/core/migrations/sqlite-migration.ts +69 -31
  65. package/src/core/sqlite-contract-serializer.ts +29 -14
  66. package/src/core/sqlite-contract-view.ts +41 -0
  67. package/src/core/sqlite-schema-verifier.ts +6 -3
  68. package/src/core/sqlite-unbound-database.ts +8 -8
  69. package/src/exports/runtime.ts +1 -0
  70. package/dist/codecs-DsC4OGmU.mjs +0 -220
  71. package/dist/codecs-DsC4OGmU.mjs.map +0 -1
  72. package/dist/descriptor-meta-DxmEeTJ-.mjs.map +0 -1
  73. package/dist/native-type-normalizer-CiSyVmMP.mjs +0 -14
  74. package/dist/native-type-normalizer-CiSyVmMP.mjs.map +0 -1
  75. package/dist/op-factory-call-DmdfD1yd.mjs.map +0 -1
  76. package/dist/planner-Ciq8p_dL.mjs +0 -622
  77. package/dist/planner-Ciq8p_dL.mjs.map +0 -1
  78. package/dist/planner-produced-sqlite-migration-0xPEm3R1.mjs.map +0 -1
  79. package/dist/sqlite-contract-serializer--iaDgC8e.mjs.map +0 -1
  80. package/dist/sqlite-migration-A0rwqPOG.mjs +0 -92
  81. package/dist/sqlite-migration-A0rwqPOG.mjs.map +0 -1
  82. package/dist/sqlite-migration-DVfhQwN_.d.mts.map +0 -1
@@ -1,44 +1,46 @@
1
1
  /**
2
2
  * SQLite migration issue planner.
3
3
  *
4
- * Takes schema issues (from `verifySqlSchema`) and emits migration IR
4
+ * Takes node-typed schema-diff issues (from the one differ see
5
+ * `buildSqlitePlanDiff` in `diff-database-schema.ts`) and emits migration IR
5
6
  * (`SqliteOpFactoryCall[]`). Strategies consume issues they recognize and
6
- * produce specialized call sequences (e.g. recreateTableStrategy absorbs
7
+ * produce specialized call sequences (e.g. `recreateTableStrategy` absorbs
7
8
  * type/nullability/default/constraint mismatches into a single recreate op);
8
- * remaining issues flow through `mapIssueToCall` for the default case.
9
+ * remaining issues flow through `mapNodeIssueToCall` for the default case.
10
+ *
11
+ * Every branch reads the diff node the issue carries (`issue.expected` /
12
+ * `issue.actual`) — never the contract, never `storageTypes`, never codec
13
+ * hooks. Column DDL resolves from the column node's `codecRef`
14
+ * (`column-ddl-rendering.ts`), never a recomputation against the contract.
9
15
  */
10
16
 
11
- import type { Contract, JsonValue } from '@prisma-next/contract/types';
12
17
  import type {
13
- CodecControlHooks,
14
18
  MigrationOperationPolicy,
15
19
  SqlPlannerConflict,
16
20
  SqlPlannerConflictLocation,
17
21
  } from '@prisma-next/family-sql/control';
18
22
  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 type { CodecRef, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast';
27
- import {
28
- DdlColumn,
29
- ForeignKeyConstraint,
30
- FunctionColumnDefault,
31
- LiteralColumnDefault,
32
- PrimaryKeyConstraint,
33
- UniqueConstraint,
34
- } from '@prisma-next/sql-relational-core/ast';
23
+ import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
24
+ import { issueOutcome, orderIssuesByDependencies } from '@prisma-next/framework-components/control';
35
25
  import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
36
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
26
+ import {
27
+ RelationalSchemaNodeKind,
28
+ type SqlColumnIR,
29
+ type SqlIndexIR,
30
+ SqlSchemaIR,
31
+ type SqlSchemaIRNode,
32
+ type SqlTableIR,
33
+ } from '@prisma-next/sql-schema-ir/types';
37
34
  import { blindCast } from '@prisma-next/utils/casts';
38
- import { ifDefined } from '@prisma-next/utils/defined';
39
35
  import type { Result } from '@prisma-next/utils/result';
40
36
  import { notOk, ok } from '@prisma-next/utils/result';
41
37
  import { CONTROL_TABLE_NAMES } from '../control-tables';
38
+ import {
39
+ columnSpecFromNode,
40
+ ddlColumnFromNode,
41
+ isInlineAutoincrementPrimaryKeyNode,
42
+ tableConstraintsFromNode,
43
+ } from './column-ddl-rendering';
42
44
  import {
43
45
  AddColumnCall,
44
46
  CreateIndexCall,
@@ -54,210 +56,93 @@ import type {
54
56
  SqliteTableSpec,
55
57
  SqliteUniqueSpec,
56
58
  } from './operations/shared';
57
- import {
58
- buildColumnDefaultSql,
59
- buildColumnTypeSql,
60
- isInlineAutoincrementPrimaryKey,
61
- resolveColumnTypeMetadata,
62
- } from './planner-ddl-builders';
63
59
  import {
64
60
  type CallMigrationStrategy,
65
- resolveNamespaceIdForIssue,
66
61
  type StrategyContext,
67
62
  sqlitePlannerStrategies,
68
- tableAt,
69
63
  } from './planner-strategies';
70
64
 
71
65
  export type { CallMigrationStrategy, StrategyContext };
72
66
 
73
67
  // ============================================================================
74
- // Issue kind ordering (dependency order)
68
+ // Subtree coalescing the planner's responsibility per the differ's contract
75
69
  // ============================================================================
76
70
 
77
- const ISSUE_KIND_ORDER: Record<string, number> = {
78
- // Drops (reconciliation clear the way for creates)
79
- extra_foreign_key: 10,
80
- extra_unique_constraint: 11,
81
- extra_primary_key: 12,
82
- extra_index: 13,
83
- extra_default: 14,
84
- extra_column: 15,
85
- extra_table: 16,
86
-
87
- // Tables before columns
88
- missing_table: 20,
89
-
90
- // Columns before constraints
91
- missing_column: 30,
92
-
93
- // Reconciliation alters (on existing objects)
94
- type_mismatch: 40,
95
- nullability_mismatch: 41,
96
- default_missing: 42,
97
- default_mismatch: 43,
98
-
99
- // Constraints after columns exist
100
- primary_key_mismatch: 50,
101
- unique_constraint_mismatch: 51,
102
- index_mismatch: 52,
103
- foreign_key_mismatch: 60,
104
- };
105
-
106
- function issueOrder(issue: SchemaIssue): number {
107
- return ISSUE_KIND_ORDER[issue.kind] ?? 99;
71
+ /**
72
+ * The generic differ is total: a missing/extra table (or column) emits an
73
+ * issue for itself AND for every node in its subtree (columns, defaults,
74
+ * constraints, indexes). `CreateTableCall`/`DropTableCall` and
75
+ * `AddColumnCall`/`DropColumnCall` already account for the whole subtree
76
+ * (reading it directly off the table/column node), so the nested issues are
77
+ * redundant — coalescing them is "the planner's responsibility" the differ's
78
+ * own contract assigns (`schema-diff.ts`). Drops any issue whose path is a
79
+ * strict descendant of a `not-found`/`not-expected` issue's path.
80
+ */
81
+ export function coalesceSubtreeIssues(
82
+ issues: readonly SchemaDiffIssue[],
83
+ ): readonly SchemaDiffIssue[] {
84
+ const collapsingPaths = issues
85
+ .filter(
86
+ (issue) => issueOutcome(issue) === 'not-found' || issueOutcome(issue) === 'not-expected',
87
+ )
88
+ .map((issue) => issue.path);
89
+ if (collapsingPaths.length === 0) return issues;
90
+ return issues.filter(
91
+ (issue) => !collapsingPaths.some((ancestor) => isStrictDescendantPath(issue.path, ancestor)),
92
+ );
108
93
  }
109
94
 
110
- function issueKey(issue: SchemaIssue): string {
111
- const table = 'table' in issue && typeof issue.table === 'string' ? issue.table : '';
112
- const column = 'column' in issue && typeof issue.column === 'string' ? issue.column : '';
113
- const name =
114
- 'indexOrConstraint' in issue && typeof issue.indexOrConstraint === 'string'
115
- ? issue.indexOrConstraint
116
- : '';
117
- return `${table}\u0000${column}\u0000${name}`;
95
+ function isStrictDescendantPath(path: readonly string[], ancestor: readonly string[]): boolean {
96
+ if (path.length <= ancestor.length) return false;
97
+ for (let i = 0; i < ancestor.length; i += 1) {
98
+ if (path[i] !== ancestor[i]) return false;
99
+ }
100
+ return true;
118
101
  }
119
102
 
120
103
  // ============================================================================
121
- // Conflict helpers
104
+ // Node helpers
122
105
  // ============================================================================
123
106
 
124
- function issueConflict(
125
- kind: SqlPlannerConflict['kind'],
126
- summary: string,
127
- location?: SqlPlannerConflict['location'],
128
- ): SqlPlannerConflict {
129
- return {
130
- kind,
131
- summary,
132
- why: 'Use `migration new` to author a custom migration for this change.',
133
- ...(location ? { location } : {}),
134
- };
107
+ export function issueNode(issue: SchemaDiffIssue): SqlSchemaIRNode | undefined {
108
+ const node = issue.expected ?? issue.actual;
109
+ if (node === undefined) return undefined;
110
+ return blindCast<
111
+ SqlSchemaIRNode,
112
+ 'every node in a SQL schema diff tree is a SqlSchemaIRNode; nodeKind is its required discriminant'
113
+ >(node);
135
114
  }
136
115
 
137
- function conflictKindForIssue(issue: SchemaIssue): SqlPlannerConflict['kind'] {
138
- switch (issue.kind) {
139
- case 'type_mismatch':
140
- return 'typeMismatch';
141
- case 'nullability_mismatch':
142
- return 'nullabilityConflict';
143
- case 'primary_key_mismatch':
144
- case 'unique_constraint_mismatch':
145
- case 'index_mismatch':
146
- case 'extra_primary_key':
147
- case 'extra_unique_constraint':
148
- return 'indexIncompatible';
149
- case 'foreign_key_mismatch':
150
- case 'extra_foreign_key':
151
- return 'foreignKeyConflict';
152
- default:
153
- return 'missingButNonAdditive';
116
+ /** Whether the expected/actual native type (resolved, or raw+many fallback) differs — mirrors `SqlColumnIR.isEqualTo`'s type comparison. */
117
+ export function columnTypeChanged(expected: SqlColumnIR, actual: SqlColumnIR): boolean {
118
+ if (expected.resolvedNativeType !== undefined && actual.resolvedNativeType !== undefined) {
119
+ return expected.resolvedNativeType !== actual.resolvedNativeType;
154
120
  }
155
- }
156
-
157
- function issueLocation(issue: SchemaIssue): SqlPlannerConflictLocation | undefined {
158
- if (issue.kind === 'enum_values_changed') return undefined;
159
- const location: {
160
- table?: string;
161
- column?: string;
162
- constraint?: string;
163
- } = {};
164
- if (issue.table) location.table = issue.table;
165
- if (issue.column) location.column = issue.column;
166
- if (issue.indexOrConstraint) location.constraint = issue.indexOrConstraint;
167
- return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
168
- }
169
-
170
- function conflictForDisallowedCall(
171
- call: SqliteOpFactoryCall,
172
- allowed: readonly string[],
173
- ): SqlPlannerConflict {
174
- const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(', ')}`;
175
- const location = locationForCall(call);
176
- return {
177
- kind: conflictKindForCall(call),
178
- summary,
179
- why: 'Use `migration new` to author a custom migration for this change.',
180
- ...(location ? { location } : {}),
181
- };
182
- }
183
-
184
- function conflictKindForCall(call: SqliteOpFactoryCall): SqlPlannerConflict['kind'] {
185
- switch (call.factoryName) {
186
- case 'createIndex':
187
- case 'dropIndex':
188
- return 'indexIncompatible';
189
- default:
190
- return 'missingButNonAdditive';
191
- }
192
- }
193
-
194
- function locationForCall(call: SqliteOpFactoryCall): SqlPlannerConflictLocation | undefined {
195
- const location: { table?: string; column?: string; index?: string } = {};
196
- if ('tableName' in call) location.table = call.tableName;
197
- if ('columnName' in call) location.column = call.columnName;
198
- if ('indexName' in call) location.index = call.indexName;
199
- return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
200
- }
201
-
202
- function isMissing(issue: SchemaIssue): boolean {
203
- if (issue.kind === 'enum_values_changed') return false;
204
- return issue.actual === undefined;
205
- }
206
-
207
- // ============================================================================
208
- // StorageTable / StorageColumn → flat SqliteTableSpec
209
- // ============================================================================
210
-
211
- /**
212
- * Resolves codec / `typeRef` / default rendering into a flat
213
- * `SqliteColumnSpec`. Mirrors Postgres's `toColumnSpec`. Once a column is
214
- * flattened, downstream Calls and operation factories never see
215
- * `StorageColumn` again — they deal in pre-rendered SQL fragments.
216
- */
217
- export function toColumnSpec(
218
- name: string,
219
- column: StorageColumn,
220
- storageTypes: Readonly<Record<string, StorageTypeInstance>>,
221
- inlineAutoincrementPrimaryKey = false,
222
- ): SqliteColumnSpec {
223
- const typeSql = buildColumnTypeSql(
224
- column,
225
- blindCast<
226
- Record<string, StorageTypeInstance>,
227
- 'buildColumnTypeSql declares its storageTypes parameter as mutable Record while the planner stores it readonly; the helper does not mutate, so the readonly→mutable narrowing is sound'
228
- >(storageTypes),
121
+ return (
122
+ expected.nativeType !== actual.nativeType || Boolean(expected.many) !== Boolean(actual.many)
229
123
  );
230
- const defaultSql = buildColumnDefaultSql(column.default);
231
- return {
232
- name,
233
- typeSql,
234
- defaultSql,
235
- nullable: column.nullable,
236
- ...(inlineAutoincrementPrimaryKey ? { inlineAutoincrementPrimaryKey: true } : {}),
237
- };
238
124
  }
239
125
 
240
126
  /**
241
- * Flattens a `StorageTable` into a `SqliteTableSpec` ready for
242
- * `CreateTableCall` / `RecreateTableCall`. Sole-column AUTOINCREMENT
243
- * primary keys are detected here and marked on the column spec so the
244
- * renderer emits `INTEGER PRIMARY KEY AUTOINCREMENT` inline.
127
+ * Builds the flat `SqliteTableSpec` `RecreateTableCall` needs from the
128
+ * expected table node — the node-sourced equivalent of the retired
129
+ * `toTableSpec` (which read a raw contract `StorageTable`). Every column's
130
+ * spec is resolved from its `codecRef` via `columnSpecFromNode`.
245
131
  */
246
- export function toTableSpec(
247
- table: StorageTable,
248
- storageTypes: Readonly<Record<string, StorageTypeInstance>>,
249
- ): SqliteTableSpec {
250
- const columns: SqliteColumnSpec[] = Object.entries(table.columns).map(([name, column]) =>
251
- toColumnSpec(name, column, storageTypes, isInlineAutoincrementPrimaryKey(table, name)),
132
+ export function tableSpecFromNode(table: SqlTableIR): SqliteTableSpec {
133
+ const columns: SqliteColumnSpec[] = Object.values(table.columns).map((c) =>
134
+ columnSpecFromNode(c, isInlineAutoincrementPrimaryKeyNode(table, c)),
252
135
  );
253
136
  const uniques: SqliteUniqueSpec[] = table.uniques.map((u) => ({
254
137
  columns: u.columns,
255
138
  ...(u.name !== undefined ? { name: u.name } : {}),
256
139
  }));
140
+ // Every FK node on the expected tree is constraint-bearing by construction
141
+ // (a persisted `foreignKeys[]` entry is a constraint; a non-constraint FK
142
+ // contributes only a backing index, never an FK node).
257
143
  const foreignKeys: SqliteForeignKeySpec[] = table.foreignKeys.map((fk) => ({
258
- columns: fk.source.columns,
259
- references: { table: fk.target.tableName, columns: fk.target.columns },
260
- constraint: fk.constraint !== false,
144
+ columns: fk.columns,
145
+ references: { table: fk.referencedTable, columns: fk.referencedColumns },
261
146
  ...(fk.name !== undefined ? { name: fk.name } : {}),
262
147
  ...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
263
148
  ...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
@@ -271,324 +156,220 @@ export function toTableSpec(
271
156
  }
272
157
 
273
158
  // ============================================================================
274
- // StorageTable / StorageColumn → DdlColumn[] + DdlTableConstraint[] (for CreateTableCall)
159
+ // Conflict helpers
275
160
  // ============================================================================
276
161
 
277
- function sqliteDefaultToDdlColumnDefault(
278
- columnDefault: StorageColumn['default'],
279
- ): DdlColumn['default'] {
280
- if (!columnDefault) return undefined;
281
- switch (columnDefault.kind) {
282
- case 'literal':
283
- return new LiteralColumnDefault(columnDefault.value);
284
- case 'function':
285
- // `autoincrement()` is not a DEFAULT clause SQLite encodes it as
286
- // `INTEGER PRIMARY KEY AUTOINCREMENT` inline on the column. Skip it
287
- // here; the renderer also has a defensive guard for the same case.
288
- if (columnDefault.expression === 'autoincrement()') return undefined;
289
- return new FunctionColumnDefault(columnDefault.expression);
290
- default: {
291
- const exhaustive: never = columnDefault;
292
- throw new Error(
293
- `sqliteDefaultToDdlColumnDefault: unhandled kind "${blindCast<{ kind: string }, 'exhaustiveness: surface the unhandled default kind'>(exhaustive).kind}"`,
294
- );
295
- }
296
- }
162
+ function issueConflict(
163
+ kind: SqlPlannerConflict['kind'],
164
+ summary: string,
165
+ location?: SqlPlannerConflict['location'],
166
+ ): SqlPlannerConflict {
167
+ return {
168
+ kind,
169
+ summary,
170
+ why: 'Use `migration new` to author a custom migration for this change.',
171
+ ...(location ? { location } : {}),
172
+ };
173
+ }
174
+
175
+ function issueLocation(tableName: string, columnName?: string): SqlPlannerConflictLocation {
176
+ return columnName !== undefined
177
+ ? { entityKind: 'table', entityName: tableName, column: columnName }
178
+ : { entityKind: 'table', entityName: tableName };
297
179
  }
298
180
 
299
181
  /**
300
- * Converts a `StorageTable` to the `DdlColumn[]` + `DdlTableConstraint[]`
301
- * pair used by `CreateTableCall`. This is the structured form consumed by
302
- * the DDL lowering path; `toTableSpec` / `toColumnSpec` remain in use for
303
- * `RecreateTableCall` and `AddColumnCall` (Phase 2).
182
+ * Conflict kind for a node kind that `recreateTableStrategy` absorbs for
183
+ * every reachable production issue. Reaching `mapNodeIssueToCall` for one of
184
+ * these means the recreate strategy didn't run mirrors the legacy
185
+ * `conflictKindForIssue` per-kind categorization.
304
186
  */
305
- export function tableToDdlParts(
306
- table: StorageTable,
307
- storageTypes: Record<string, StorageTypeInstance>,
308
- ): { columns: DdlColumn[]; constraints: DdlTableConstraint[] } {
309
- const columns: DdlColumn[] = Object.entries(table.columns).map(([name, column]) => {
310
- const inlineAutoincrement = isInlineAutoincrementPrimaryKey(table, name);
311
- const typeSql = buildColumnTypeSql(
312
- column,
313
- blindCast<
314
- Record<string, StorageTypeInstance>,
315
- 'buildColumnTypeSql declares its storageTypes parameter as mutable Record while the planner stores it readonly; the helper does not mutate, so the readonly→mutable narrowing is sound'
316
- >(storageTypes),
317
- );
318
-
319
- if (inlineAutoincrement) {
320
- // `DdlColumn` has no SQLite-specific autoincrement flag, so the full
321
- // `PRIMARY KEY AUTOINCREMENT` clause is embedded in the `type` string.
322
- // The DDL renderer (`ddl-renderer.ts`) substring-detects `AUTOINCREMENT`
323
- // to suppress the normal NOT NULL / PRIMARY KEY / DEFAULT clause rendering
324
- // and emit the entire type string verbatim. Both sites must stay in sync.
325
- // The structural fix (a SQLite-specific column option) is tracked in TML-2866.
326
- return new DdlColumn({ name, type: `${typeSql} PRIMARY KEY AUTOINCREMENT` });
327
- }
328
- const colDefault = sqliteDefaultToDdlColumnDefault(column.default);
329
- const resolved = resolveColumnTypeMetadata(
330
- column,
331
- blindCast<
332
- Record<string, StorageTypeInstance>,
333
- 'resolveColumnTypeMetadata declares its storageTypes parameter as mutable Record while the planner stores it readonly; the helper does not mutate, so the readonly→mutable narrowing is sound'
334
- >(storageTypes),
335
- );
336
- const codecRef: CodecRef | undefined = resolved.codecId
337
- ? {
338
- codecId: resolved.codecId,
339
- ...(resolved.typeParams !== undefined
340
- ? {
341
- typeParams: blindCast<
342
- JsonValue,
343
- 'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed (non-undefined) value lands in CodecRef.typeParams which is JsonValue'
344
- >(resolved.typeParams),
345
- }
346
- : {}),
347
- }
348
- : undefined;
349
- return new DdlColumn({
350
- name,
351
- type: typeSql,
352
- ...(!column.nullable ? { notNull: true } : {}),
353
- ...(colDefault !== undefined ? { default: colDefault } : {}),
354
- ...(codecRef !== undefined ? { codecRef } : {}),
355
- });
356
- });
357
-
358
- const constraints: DdlTableConstraint[] = [];
359
-
360
- const hasInlinePk = Object.entries(table.columns).some(([name]) =>
361
- isInlineAutoincrementPrimaryKey(table, name),
362
- );
363
- if (table.primaryKey && !hasInlinePk) {
364
- constraints.push(new PrimaryKeyConstraint({ columns: table.primaryKey.columns }));
187
+ function absorbedConflictKind(nodeKind: string): SqlPlannerConflict['kind'] {
188
+ switch (nodeKind) {
189
+ case RelationalSchemaNodeKind.primaryKey:
190
+ case RelationalSchemaNodeKind.unique:
191
+ return 'indexIncompatible';
192
+ case RelationalSchemaNodeKind.foreignKey:
193
+ return 'foreignKeyConflict';
194
+ default:
195
+ return 'missingButNonAdditive';
365
196
  }
197
+ }
366
198
 
367
- for (const u of table.uniques) {
368
- constraints.push(
369
- new UniqueConstraint({
370
- columns: u.columns,
371
- ...(u.name !== undefined ? { name: u.name } : {}),
372
- }),
373
- );
374
- }
199
+ // ============================================================================
200
+ // StorageTable / StorageColumn → flat SqliteTableSpec / DdlColumn helpers
201
+ // ============================================================================
375
202
 
376
- for (const fk of table.foreignKeys) {
377
- if (fk.constraint === false) continue;
378
- constraints.push(
379
- new ForeignKeyConstraint({
380
- columns: fk.source.columns,
381
- refTable: fk.target.tableName,
382
- refColumns: fk.target.columns,
383
- ...ifDefined('name', fk.name),
384
- ...ifDefined('onDelete', fk.onDelete),
385
- ...ifDefined('onUpdate', fk.onUpdate),
386
- }),
387
- );
203
+ /**
204
+ * Builds the `CreateTableCall` + per-index `CreateIndexCall`s for a
205
+ * newly-expected table. Reads only the table node's own children — indexes
206
+ * (declared + FK-backing, deduped) are already merged and ordered at
207
+ * derivation (`contractToSchemaIR`'s `convertTable`).
208
+ */
209
+ function buildCreateTableCalls(table: SqlTableIR): SqliteOpFactoryCall[] {
210
+ const columns = Object.values(table.columns).map((c) =>
211
+ ddlColumnFromNode(c, isInlineAutoincrementPrimaryKeyNode(table, c)),
212
+ );
213
+ const hasInlinePk = Object.values(table.columns).some((c) =>
214
+ isInlineAutoincrementPrimaryKeyNode(table, c),
215
+ );
216
+ const constraints = tableConstraintsFromNode(table, hasInlinePk);
217
+ const calls: SqliteOpFactoryCall[] = [
218
+ new CreateTableCall(table.name, columns, constraints.length > 0 ? constraints : undefined),
219
+ ];
220
+ for (const index of table.indexes) {
221
+ const indexName = index.name ?? defaultIndexName(table.name, index.columns);
222
+ calls.push(new CreateIndexCall(table.name, indexName, index.columns));
388
223
  }
389
-
390
- return { columns, constraints };
224
+ return calls;
391
225
  }
392
226
 
393
227
  // ============================================================================
394
- // Issue planner
228
+ // Issue → Call mapping (per-issue default path)
395
229
  // ============================================================================
396
230
 
397
- export interface IssuePlannerOptions {
398
- readonly issues: readonly SchemaIssue[];
399
- readonly toContract: Contract<SqlStorage>;
400
- readonly fromContract: Contract<SqlStorage> | null;
401
- readonly codecHooks: ReadonlyMap<string, CodecControlHooks>;
402
- readonly storageTypes: Readonly<Record<string, StorageTypeInstance>>;
403
- readonly schema?: SqlSchemaIR;
404
- readonly policy?: MigrationOperationPolicy;
405
- readonly frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
406
- readonly strategies?: readonly CallMigrationStrategy[];
231
+ function mapTableIssue(
232
+ issue: SchemaDiffIssue,
233
+ ): Result<readonly SqliteOpFactoryCall[], SqlPlannerConflict> {
234
+ if (issueOutcome(issue) === 'not-found') {
235
+ const table = blindCast<
236
+ SqlTableIR,
237
+ 'a not-found table issue always carries the expected table node'
238
+ >(issue.expected);
239
+ return ok(buildCreateTableCalls(table));
240
+ }
241
+ if (issueOutcome(issue) === 'not-expected') {
242
+ const table = blindCast<
243
+ SqlTableIR,
244
+ 'a not-expected table issue always carries the actual table node'
245
+ >(issue.actual);
246
+ // Runner-owned control tables must never be dropped.
247
+ if (CONTROL_TABLE_NAMES.has(table.name)) return ok([]);
248
+ return ok([new DropTableCall(table.name)]);
249
+ }
250
+ // Unreachable: SqlTableIR.isEqualTo is identity, so a paired table can
251
+ // never mismatch — kept for exhaustiveness against a future node change.
252
+ return notOk(
253
+ issueConflict('unsupportedOperation', `Unexpected table drift: ${issue.path.join('/')}`),
254
+ );
407
255
  }
408
256
 
409
- export interface IssuePlannerValue {
410
- readonly calls: readonly SqliteOpFactoryCall[];
257
+ function mapColumnIssue(
258
+ issue: SchemaDiffIssue,
259
+ ctx: StrategyContext,
260
+ ): Result<readonly SqliteOpFactoryCall[], SqlPlannerConflict> {
261
+ const tableName = issue.path[1];
262
+ if (tableName === undefined) {
263
+ return notOk(
264
+ issueConflict(
265
+ 'unsupportedOperation',
266
+ `Column issue has no table in its path: ${issue.path.join('/')}`,
267
+ ),
268
+ );
269
+ }
270
+ if (issueOutcome(issue) === 'not-found') {
271
+ const column = blindCast<
272
+ SqlColumnIR,
273
+ 'a not-found column issue always carries the expected column node'
274
+ >(issue.expected);
275
+ // A sole-autoincrement-PK column is always part of the table's own
276
+ // CREATE (never a bare ADD COLUMN — PK changes go through
277
+ // `recreateTableStrategy`), but the check is cheap and keeps this path
278
+ // honest against the table node rather than assuming it.
279
+ const table = ctx.expected.tables[tableName];
280
+ const inline = table !== undefined && isInlineAutoincrementPrimaryKeyNode(table, column);
281
+ return ok([new AddColumnCall(tableName, columnSpecFromNode(column, inline))]);
282
+ }
283
+ if (issueOutcome(issue) === 'not-expected') {
284
+ const column = blindCast<
285
+ SqlColumnIR,
286
+ 'a not-expected column issue always carries the actual column node'
287
+ >(issue.actual);
288
+ return ok([new DropColumnCall(tableName, column.name)]);
289
+ }
290
+ // not-equal: absorbed by recreateTableStrategy for every reachable
291
+ // production issue (SQLite can't ALTER a column type/nullability in
292
+ // place). Reaching here means the strategy didn't run — conflict.
293
+ const expected = blindCast<
294
+ SqlColumnIR,
295
+ 'a not-equal column issue always carries the expected column node'
296
+ >(issue.expected);
297
+ const actual = blindCast<
298
+ SqlColumnIR,
299
+ 'a not-equal column issue always carries the actual column node'
300
+ >(issue.actual);
301
+ const kind = columnTypeChanged(expected, actual) ? 'typeMismatch' : 'nullabilityConflict';
302
+ return notOk(issueConflict(kind, issue.path.join('/'), issueLocation(tableName, expected.name)));
411
303
  }
412
304
 
413
- const DEFAULT_POLICY: MigrationOperationPolicy = {
414
- allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'],
415
- };
416
-
417
- function emptySchemaIR(): SqlSchemaIR {
418
- return { tables: {} };
305
+ function mapIndexIssue(
306
+ issue: SchemaDiffIssue,
307
+ ): Result<readonly SqliteOpFactoryCall[], SqlPlannerConflict> {
308
+ const tableName = issue.path[1];
309
+ if (tableName === undefined) {
310
+ return notOk(
311
+ issueConflict(
312
+ 'unsupportedOperation',
313
+ `Index issue has no table in its path: ${issue.path.join('/')}`,
314
+ ),
315
+ );
316
+ }
317
+ if (issueOutcome(issue) === 'not-found') {
318
+ const index = blindCast<
319
+ SqlIndexIR,
320
+ 'a not-found index issue always carries the expected index node'
321
+ >(issue.expected);
322
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns);
323
+ return ok([new CreateIndexCall(tableName, indexName, index.columns)]);
324
+ }
325
+ if (issueOutcome(issue) === 'not-expected') {
326
+ const index = blindCast<
327
+ SqlIndexIR,
328
+ 'a not-expected index issue always carries the actual index node'
329
+ >(issue.actual);
330
+ const indexName = index.name ?? defaultIndexName(tableName, index.columns);
331
+ return ok([new DropIndexCall(tableName, indexName)]);
332
+ }
333
+ // not-equal: index type/options/uniqueness drift. SQLite can't ALTER an
334
+ // index in place and the legacy planner never absorbed this into a
335
+ // recreate either — surfaces as a conflict, matching `index_mismatch`.
336
+ return notOk(issueConflict('indexIncompatible', issue.path.join('/'), issueLocation(tableName)));
419
337
  }
420
338
 
421
- // ============================================================================
422
- // Issue → Call mapping (per-issue default path)
423
- // ============================================================================
424
-
425
- function mapIssueToCall(
426
- issue: SchemaIssue,
339
+ export function mapNodeIssueToCall(
340
+ issue: SchemaDiffIssue,
427
341
  ctx: StrategyContext,
428
342
  ): Result<readonly SqliteOpFactoryCall[], SqlPlannerConflict> {
429
- switch (issue.kind) {
430
- case 'missing_table': {
431
- if (!issue.table) {
432
- return notOk(
433
- issueConflict('unsupportedOperation', 'Missing table issue has no table name'),
434
- );
435
- }
436
- const namespaceId = resolveNamespaceIdForIssue(issue);
437
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
438
- if (!contractTable) {
439
- return notOk(
440
- issueConflict(
441
- 'unsupportedOperation',
442
- `Table "${issue.table}" in namespace "${namespaceId}" reported missing but not found in destination contract`,
443
- ),
444
- );
445
- }
446
- const { columns: ddlColumns, constraints: ddlConstraints } = tableToDdlParts(
447
- contractTable,
448
- ctx.storageTypes,
449
- );
450
- const calls: SqliteOpFactoryCall[] = [
451
- new CreateTableCall(
452
- issue.table,
453
- ddlColumns,
454
- ddlConstraints.length > 0 ? ddlConstraints : undefined,
455
- ),
456
- ];
457
- const declaredIndexColumnKeys = new Set<string>();
458
- for (const index of contractTable.indexes) {
459
- const indexName = index.name ?? defaultIndexName(issue.table, index.columns);
460
- declaredIndexColumnKeys.add(index.columns.join(','));
461
- calls.push(new CreateIndexCall(issue.table, indexName, index.columns));
462
- }
463
- for (const fk of contractTable.foreignKeys) {
464
- if (fk.index === false) continue;
465
- if (declaredIndexColumnKeys.has(fk.source.columns.join(','))) continue;
466
- const indexName = defaultIndexName(issue.table, fk.source.columns);
467
- calls.push(new CreateIndexCall(issue.table, indexName, fk.source.columns));
468
- }
469
- return ok(calls);
470
- }
471
-
472
- case 'missing_column': {
473
- if (!issue.table || !issue.column) {
474
- return notOk(
475
- issueConflict('unsupportedOperation', 'Missing column issue has no table/column name'),
476
- );
477
- }
478
- const namespaceId = resolveNamespaceIdForIssue(issue);
479
- const contractTable2 = tableAt(ctx.toContract.storage, namespaceId, issue.table);
480
- const column = contractTable2?.columns[issue.column];
481
- if (!column) {
482
- return notOk(
483
- issueConflict(
484
- 'unsupportedOperation',
485
- `Column "${issue.table}"."${issue.column}" not in destination contract`,
486
- ),
487
- );
488
- }
489
- const contractTable = contractTable2;
490
- const columnSpec = toColumnSpec(
491
- issue.column,
492
- column,
493
- ctx.storageTypes,
494
- contractTable ? isInlineAutoincrementPrimaryKey(contractTable, issue.column) : false,
495
- );
496
- return ok([new AddColumnCall(issue.table, columnSpec)]);
497
- }
498
-
499
- case 'index_mismatch': {
500
- if (!issue.table) {
501
- return notOk(issueConflict('indexIncompatible', 'Index issue has no table name'));
502
- }
503
- if (!isMissing(issue) || !issue.expected) {
504
- return notOk(
505
- issueConflict(
506
- 'indexIncompatible',
507
- `Index on "${issue.table}" differs (expected: ${issue.expected}, actual: ${issue.actual})`,
508
- { table: issue.table },
509
- ),
510
- );
511
- }
512
- const namespaceId = resolveNamespaceIdForIssue(issue);
513
- const columns = issue.expected.split(', ');
514
- const contractTable = tableAt(ctx.toContract.storage, namespaceId, issue.table);
515
- if (!contractTable) {
516
- return notOk(
517
- issueConflict(
518
- 'unsupportedOperation',
519
- `Table "${issue.table}" not found in destination contract`,
520
- ),
521
- );
522
- }
523
- const explicitIndex = contractTable.indexes.find(
524
- (idx) => idx.columns.join(',') === columns.join(','),
525
- );
526
- const indexName = explicitIndex?.name ?? defaultIndexName(issue.table, columns);
527
- return ok([new CreateIndexCall(issue.table, indexName, columns)]);
528
- }
529
-
530
- case 'extra_table': {
531
- if (!issue.table) {
532
- return notOk(issueConflict('unsupportedOperation', 'Extra table issue has no table name'));
533
- }
534
- // Runner-owned control tables must never be dropped.
535
- if (CONTROL_TABLE_NAMES.has(issue.table)) return ok([]);
536
- return ok([new DropTableCall(issue.table)]);
537
- }
538
-
539
- case 'extra_column': {
540
- if (!issue.table || !issue.column) {
541
- return notOk(
542
- issueConflict('unsupportedOperation', 'Extra column issue has no table/column name'),
543
- );
544
- }
545
- return ok([new DropColumnCall(issue.table, issue.column)]);
546
- }
547
-
548
- case 'extra_index': {
549
- if (!issue.table || !issue.indexOrConstraint) {
550
- return notOk(
551
- issueConflict('unsupportedOperation', 'Extra index issue has no table/index name'),
552
- );
553
- }
554
- return ok([new DropIndexCall(issue.table, issue.indexOrConstraint)]);
555
- }
556
-
557
- // SQLite has no enum types (capability `sql.enums: false`). The verifier
558
- // should never emit `enum_values_changed` against a SQLite schema, so if
559
- // we receive one it is a verifier bug — surface it as an explicit
560
- // conflict rather than silently dropping it.
561
- case 'enum_values_changed':
343
+ const node = issueNode(issue);
344
+ if (node === undefined) {
345
+ return notOk(
346
+ issueConflict(
347
+ 'unsupportedOperation',
348
+ `Issue carries neither an expected nor an actual node: ${issue.path.join('/')}`,
349
+ ),
350
+ );
351
+ }
352
+ switch (node.nodeKind) {
353
+ case RelationalSchemaNodeKind.table:
354
+ return mapTableIssue(issue);
355
+ case RelationalSchemaNodeKind.column:
356
+ return mapColumnIssue(issue, ctx);
357
+ case RelationalSchemaNodeKind.index:
358
+ return mapIndexIssue(issue);
359
+ case RelationalSchemaNodeKind.columnDefault:
360
+ case RelationalSchemaNodeKind.primaryKey:
361
+ case RelationalSchemaNodeKind.foreignKey:
362
+ case RelationalSchemaNodeKind.unique:
363
+ return notOk(issueConflict(absorbedConflictKind(node.nodeKind), issue.path.join('/')));
364
+ case RelationalSchemaNodeKind.check:
562
365
  return notOk(
563
366
  issueConflict(
564
367
  'unsupportedOperation',
565
- 'Received enum_values_changed against a SQLite schema (sql.enums: false) — verifier bug',
368
+ `SQLite does not support CHECK constraint DDL: ${issue.path.join('/')}`,
566
369
  ),
567
370
  );
568
-
569
- // Everything below is absorbed by recreateTableStrategy. If it falls
570
- // through here, policy or context didn't allow the recreate — surface as
571
- // a conflict.
572
- case 'type_mismatch':
573
- case 'nullability_mismatch':
574
- case 'default_mismatch':
575
- case 'default_missing':
576
- case 'extra_default':
577
- case 'primary_key_mismatch':
578
- case 'unique_constraint_mismatch':
579
- case 'foreign_key_mismatch':
580
- case 'extra_foreign_key':
581
- case 'extra_unique_constraint':
582
- case 'extra_primary_key':
583
- return notOk(issueConflict(conflictKindForIssue(issue), issue.message, issueLocation(issue)));
584
-
585
371
  default:
586
- return notOk(
587
- issueConflict(
588
- 'unsupportedOperation',
589
- `Unhandled issue kind: ${(issue as SchemaIssue).kind}`,
590
- ),
591
- );
372
+ return notOk(issueConflict('unsupportedOperation', `Unhandled node kind: ${node.nodeKind}`));
592
373
  }
593
374
  }
594
375
 
@@ -630,20 +411,35 @@ function classifyCall(call: SqliteOpFactoryCall): CallCategory | null {
630
411
  // Top-level planIssues
631
412
  // ============================================================================
632
413
 
414
+ export interface IssuePlannerOptions {
415
+ readonly issues: readonly SchemaDiffIssue[];
416
+ /** The desired ("end") tree — resolved leaf values, incl. `codecRef`. */
417
+ readonly expected?: SqlSchemaIR;
418
+ /** The live ("start") tree. */
419
+ readonly actual?: SqlSchemaIR;
420
+ readonly policy?: MigrationOperationPolicy;
421
+ readonly frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
422
+ readonly strategies?: readonly CallMigrationStrategy[];
423
+ }
424
+
425
+ export interface IssuePlannerValue {
426
+ readonly calls: readonly SqliteOpFactoryCall[];
427
+ }
428
+
429
+ const DEFAULT_POLICY: MigrationOperationPolicy = {
430
+ allowedOperationClasses: ['additive', 'widening', 'destructive', 'data'],
431
+ };
432
+
633
433
  export function planIssues(
634
434
  options: IssuePlannerOptions,
635
435
  ): Result<IssuePlannerValue, readonly SqlPlannerConflict[]> {
636
436
  const policyProvided = options.policy !== undefined;
637
437
  const policy = options.policy ?? DEFAULT_POLICY;
638
- const schema = options.schema ?? emptySchemaIR();
639
438
  const frameworkComponents = options.frameworkComponents ?? [];
640
439
 
641
440
  const context: StrategyContext = {
642
- toContract: options.toContract,
643
- fromContract: options.fromContract,
644
- codecHooks: options.codecHooks,
645
- storageTypes: options.storageTypes,
646
- schema,
441
+ expected: options.expected ?? emptySchemaIR(),
442
+ actual: options.actual ?? emptySchemaIR(),
647
443
  policy,
648
444
  frameworkComponents,
649
445
  };
@@ -666,19 +462,18 @@ export function planIssues(
666
462
  }
667
463
  }
668
464
 
669
- const sorted = [...remaining].sort((a, b) => {
670
- const kindDelta = issueOrder(a) - issueOrder(b);
671
- if (kindDelta !== 0) return kindDelta;
672
- const keyA = issueKey(a);
673
- const keyB = issueKey(b);
674
- return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
675
- });
465
+ // Dependency-graph order: a topological sort over the issues' `dependsOn`
466
+ // cross-links and containment edges (edge direction from the ordering law's
467
+ // presence rule), path-tiebroken for determinism. `classifyCall` bucketing
468
+ // downstream fixes the coarse create/recreate/drop sequence; this settles
469
+ // the order within each bucket.
470
+ const sorted = orderIssuesByDependencies(remaining);
676
471
 
677
472
  const defaultCalls: SqliteOpFactoryCall[] = [];
678
473
  const conflicts: SqlPlannerConflict[] = [];
679
474
 
680
475
  for (const issue of sorted) {
681
- const result = mapIssueToCall(issue, context);
476
+ const result = mapNodeIssueToCall(issue, context);
682
477
  if (result.ok) {
683
478
  defaultCalls.push(...result.value);
684
479
  } else {
@@ -732,3 +527,43 @@ export function planIssues(
732
527
 
733
528
  return ok({ calls });
734
529
  }
530
+
531
+ function emptySchemaIR(): SqlSchemaIR {
532
+ return new SqlSchemaIR({ tables: {} });
533
+ }
534
+
535
+ function conflictForDisallowedCall(
536
+ call: SqliteOpFactoryCall,
537
+ allowed: readonly string[],
538
+ ): SqlPlannerConflict {
539
+ const summary = `Operation "${call.label}" requires class "${call.operationClass}", but policy allows only: ${allowed.join(', ')}`;
540
+ const location = locationForCall(call);
541
+ return {
542
+ kind: conflictKindForCall(call),
543
+ summary,
544
+ why: 'Use `migration new` to author a custom migration for this change.',
545
+ ...(location ? { location } : {}),
546
+ };
547
+ }
548
+
549
+ function conflictKindForCall(call: SqliteOpFactoryCall): SqlPlannerConflict['kind'] {
550
+ switch (call.factoryName) {
551
+ case 'createIndex':
552
+ case 'dropIndex':
553
+ return 'indexIncompatible';
554
+ default:
555
+ return 'missingButNonAdditive';
556
+ }
557
+ }
558
+
559
+ function locationForCall(call: SqliteOpFactoryCall): SqlPlannerConflictLocation | undefined {
560
+ const location: { entityKind?: string; entityName?: string; column?: string; index?: string } =
561
+ {};
562
+ if ('tableName' in call) {
563
+ location.entityKind = 'table';
564
+ location.entityName = call.tableName;
565
+ }
566
+ if ('columnName' in call) location.column = call.columnName;
567
+ if ('indexName' in call) location.index = call.indexName;
568
+ return Object.keys(location).length > 0 ? (location as SqlPlannerConflictLocation) : undefined;
569
+ }