Skip to content

fix(Migrator): preserve indexes, triggers, table options and views across table rebuilds#236

Open
h2zi wants to merge 2 commits into
go-gorm:masterfrom
h2zi:fix-recreate-table-preserve-schema
Open

fix(Migrator): preserve indexes, triggers, table options and views across table rebuilds#236
h2zi wants to merge 2 commits into
go-gorm:masterfrom
h2zi:fix-recreate-table-preserve-schema

Conversation

@h2zi

@h2zi h2zi commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What this PR does

recreateTable implements ALTER TABLE operations by creating a modified copy of the table, moving the data over, dropping the original and renaming the copy back. Three kinds of schema objects were silently lost along the way:

Indexes and triggers. DROP TABLE removes them together with the old table. A full AutoMigrate happens to recreate the indexes declared in the model afterwards, but calling AlterColumn, DropColumn or the constraint methods directly loses every index and trigger on the table without any error.

Table options after the column list. The DDL parser only kept the head and the column list, so WITHOUT ROWID and STRICT disappeared on rebuild and the table silently became a plain rowid table.

Views referencing the table (#225). The RENAME step re-resolves referencing views while the original table name doesn't exist yet (it was just dropped), so the whole operation fails with error in view ...: no such table.

The fix

  • Before the rebuild, save the sql of every index and trigger on the table from sqlite_master, and replay them after the rename. Indexes referencing a dropped column can no longer apply and are skipped.
  • The DDL parser now captures everything after the closing parenthesis into a suffix field and compile() emits it again, preserving WITHOUT ROWID/STRICT.
  • The rename runs under PRAGMA legacy_alter_table, which skips the view re-resolution; the view points at the original table name and is valid again right after the rename. The PRAGMA is switched off immediately afterwards, whether the rename succeeds or fails.

Tests

  • TestRecreateTablePreservesIndexesAndTriggers: an index on a kept column and a trigger survive DropColumn (and the trigger still fires), while an index on the dropped column is removed.
  • TestRecreateTablePreservesTableOptions: WITHOUT ROWID survives a rebuild.
  • TestRecreateTableWithView: rebuilding a table referenced by a view succeeds and the view still works (reproduces Failing to recreate altered table when referenced by view #225).

Fixes #225

🤖 Generated with Claude Code

…ross table rebuilds

recreateTable implements ALTER TABLE operations by creating a modified
copy, moving the data over, dropping the original table and renaming
the copy back. Three things were lost along the way:

- indexes and triggers: DROP TABLE removes them together with the old
  table. AutoMigrate happens to recreate indexes declared in the model
  afterwards, but calling AlterColumn/DropColumn/*Constraint directly
  silently loses every index and trigger on the table. Their DDL is now
  saved from sqlite_master before the rebuild and replayed afterwards;
  indexes referencing a dropped column can no longer apply and are
  skipped.

- table options after the column list: the DDL parser only kept the
  head and the column list, so WITHOUT ROWID and STRICT silently
  disappeared and the rebuilt table became a plain rowid table. The
  parser now keeps the trailing options and compile() emits them again.

- views referencing the table: the RENAME step re-resolves referencing
  views while the original table name doesn't exist yet, failing with
  "error in view ...: no such table" (go-gorm#225). The rename now runs under
  PRAGMA legacy_alter_table, which skips that re-resolution; the view
  points at the original name and is valid again right after the
  rename.

Fixes go-gorm#225
Copilot AI review requested due to automatic review settings July 7, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the SQLite migrator’s table-rebuild strategy used for ALTER TABLE emulation by preserving schema artifacts that were previously lost or could cause rebuild failures (indexes/triggers, table options like WITHOUT ROWID/STRICT, and views referencing the rebuilt table).

Changes:

  • Persist and replay index/trigger DDL around recreateTable() so rebuilds don’t silently drop them.
  • Extend the CREATE TABLE DDL parser to retain and re-emit trailing table options (suffix).
  • Avoid view re-resolution failures during rename by toggling PRAGMA legacy_alter_table for the rename step, plus add focused regression tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
migrator.go Saves/replays auxiliary DDL and uses legacy_alter_table around the rename to keep views valid during rebuild.
ddlmod.go Captures and re-emits CREATE TABLE trailing options via a new suffix field.
migrator_test.go Adds regression tests covering preservation of indexes/triggers, table options, and view-referenced rebuilds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread migrator.go
Comment on lines +453 to +457
// recreate the saved indexes and triggers; ones referencing a
// column that no longer exists cannot apply anymore and are skipped
for _, aux := range auxDDLs {
_ = tx.Exec(aux).Error
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in 8f94608. The replay loop now only skips no such column errors (an index or trigger referencing the dropped column, which is the one expected casualty of the rebuild); any other failure aborts the migration and rolls back the transaction.

Comment thread migrator_test.go
Comment on lines +49 to +51
var names []string
db.Raw("SELECT name FROM sqlite_master WHERE tbl_name = 'rebuild_idx_table' AND type IN ('index','trigger')").Scan(&names)
got := strings.Join(names, ",")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f94608 — the test now fails on the query error instead of asserting against zero values.

Comment thread migrator_test.go Outdated
Comment on lines +66 to +67
var auditCount int
db.Raw("SELECT count(*) FROM rebuild_audit").Scan(&auditCount)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f94608 — same treatment as above.

Comment thread migrator_test.go Outdated
Comment on lines +92 to +93
var ddl string
db.Raw("SELECT sql FROM sqlite_master WHERE type='table' AND name='rebuild_opts_table'").Scan(&ddl)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f94608 — same treatment as above.

Address review feedback: replaying saved index/trigger DDL swallowed
every error, silently dropping objects for unrelated reasons. Only
"no such column" (an index/trigger referencing the dropped column)
is skipped now; any other failure aborts the migration.

Also fail the tests on query errors instead of asserting against
zero values, and close the pool on cleanup so the shared in-memory
database is torn down between -count=N runs.
h2zi added a commit to libtnb/sqlite that referenced this pull request Jul 7, 2026
Replaying saved index/trigger DDL after a table rebuild swallowed every
error, silently dropping objects for unrelated reasons. Only "no such
column" (an index or trigger referencing the dropped column) is skipped
now; any other failure aborts the migration. Also fail tests on query
errors instead of asserting against zero values.

Mirrors the review feedback on go-gorm/sqlite#236.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failing to recreate altered table when referenced by view

2 participants