Skip to content

Commit 602d8f4

Browse files
dmealingclaude
andcommitted
fix(cli/migrate): pre-drop dependent views before table recreates
SQLite's ALTER TABLE ... RENAME re-parses dependent view definitions and errors with "no such table" if the referenced source table is mid-recreate (the recreate-and-copy pattern temporarily drops the source table and creates a __new_<table>, then RENAMEs). Even though the rename restores the source table name, SQLite's parser sees the intermediate state and aborts the rename. Surfaced when applying a real mikes-website migration: every dropped table that backed a projection view (purchases→v_customer_summary, v_purchase_summary; weeks/workouts/exercises→v_program_summary) failed at the RENAME step, leaving the DB in a half-migrated state. Fix: emit `DROP VIEW IF EXISTS <view>;` for every projection view at the very top of the combined migration, before any table SQL. The viewUpSql block (already at the tail of the migration) recreates them fresh. Only emitted when hasTableChanges is true — pure view-only migrations are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 88aaa0a commit 602d8f4

1 file changed

Lines changed: 20 additions & 6 deletions

File tree

typescript/packages/cli/src/commands/migrate.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,15 +256,29 @@ export async function migrateCommand(args: string[], cwd: string): Promise<numbe
256256

257257
// Combine table + view SQL into a single migration if no errors.
258258
if (exitCode === 0) {
259-
const upParts = [tableSql?.up, viewUpSql].filter(Boolean);
260-
const combinedUp = upParts.join("\n\n");
261-
// Down SQL: DROP VIEW statements for any views we created.
262-
const viewDownSql = viewResult.migrations
259+
// Extract view names from the CREATE VIEW statements so we can drop them
260+
// both at the top (before table changes) and in the down migration.
261+
// Reason for the top-side drops: SQLite's ALTER TABLE ... RENAME re-parses
262+
// dependent views and errors if their referenced source table is mid-
263+
// recreate (the recreate-and-copy pattern temporarily drops then
264+
// recreates the source table under its target name). Pre-dropping every
265+
// dependent view lets the table recreate proceed; the viewUpSql block
266+
// below recreates them fresh.
267+
const viewNames = viewResult.migrations
263268
.map((s) => {
264269
const m = /CREATE(?:\s+OR\s+REPLACE)?\s+VIEW\s+(\S+)/i.exec(s);
265-
return m ? `DROP VIEW IF EXISTS ${m[1]};` : "";
270+
return m ? m[1] : undefined;
266271
})
267-
.filter(Boolean)
272+
.filter((n): n is string => Boolean(n));
273+
const viewPreDropSql = hasTableChanges && viewNames.length > 0
274+
? viewNames.map((n) => `DROP VIEW IF EXISTS ${n};`).join("\n")
275+
: "";
276+
277+
const upParts = [viewPreDropSql, tableSql?.up, viewUpSql].filter(Boolean);
278+
const combinedUp = upParts.join("\n\n");
279+
// Down SQL: DROP VIEW statements for any views we created.
280+
const viewDownSql = viewNames
281+
.map((n) => `DROP VIEW IF EXISTS ${n};`)
268282
.join("\n");
269283
const downParts = [viewDownSql, tableSql?.down].filter(Boolean);
270284
const combinedDown = downParts.join("\n\n");

0 commit comments

Comments
 (0)