Skip to content

Commit

Permalink
sqlite: add migration test
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Jan 15, 2025
1 parent eb50db0 commit a975b8d
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

# Added a test for migrations to ensure consistency between database schemas
70 changes: 47 additions & 23 deletions persist/sqlite/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,28 @@ func (s *Store) initNewDatabase(target int64) error {
}

func (s *Store) upgradeDatabase(current, target int64) error {
log := s.log.Named("migrations")
log.Info("migrating database", zap.Int64("current", current), zap.Int64("target", target))

return s.transaction(func(tx *txn) error {
// defer foreign key constraints until commit
if _, err := tx.Exec("PRAGMA defer_foreign_keys=ON"); err != nil {
return fmt.Errorf("failed to enable foreign key deferral: %w", err)
}

for _, fn := range migrations[current-1:] {
current++
start := time.Now()
if err := fn(tx, log.With(zap.Int64("version", current))); err != nil {
return fmt.Errorf("failed to migrate database to version %v: %w", current, err)
}
// check that no foreign key constraints were violated
if err := tx.QueryRow("PRAGMA foreign_key_check").Scan(); !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("foreign key constraints are not satisfied")
log := s.log.Named("migrations").With(zap.Int64("target", target))
for ; current < target; current++ {
version := current + 1 // initial schema is version 1, migration 0 is version 2, etc.
log := log.With(zap.Int64("version", version))
start := time.Now()
fn := migrations[current-1]
err := s.transaction(func(tx *txn) error {
if _, err := tx.Exec("PRAGMA defer_foreign_keys=ON"); err != nil {
return fmt.Errorf("failed to enable foreign key deferral: %w", err)
} else if err := fn(tx, log); err != nil {
return err
} else if err := foreignKeyCheck(tx, log); err != nil {
return fmt.Errorf("failed foreign key check: %w", err)
}
log.Debug("migration complete", zap.Int64("current", current), zap.Int64("target", target), zap.Duration("elapsed", time.Since(start)))
return setDBVersion(tx, version)
})
if err != nil {
return fmt.Errorf("migration %d failed: %w", version, err)
}

// set the final database version
return setDBVersion(tx, target)
})
log.Info("migration complete", zap.Duration("elapsed", time.Since(start)))
}
return nil
}

func (s *Store) init() error {
Expand All @@ -77,3 +74,30 @@ func (s *Store) init() error {
// nothing to do
return nil
}

func foreignKeyCheck(txn *txn, log *zap.Logger) error {
rows, err := txn.Query("PRAGMA foreign_key_check")
if err != nil {
return fmt.Errorf("failed to run foreign key check: %w", err)
}
defer rows.Close()
var hasErrors bool
for rows.Next() {
var table string
var rowid sql.NullInt64
var fkTable string
var fkRowid sql.NullInt64

if err := rows.Scan(&table, &rowid, &fkTable, &fkRowid); err != nil {
return fmt.Errorf("failed to scan foreign key check result: %w", err)
}
hasErrors = true
log.Error("foreign key constraint violated", zap.String("table", table), zap.Int64("rowid", rowid.Int64), zap.String("fkTable", fkTable), zap.Int64("fkRowid", fkRowid.Int64))
}
if err := rows.Err(); err != nil {
return fmt.Errorf("failed to iterate foreign key check results: %w", err)
} else if hasErrors {
return errors.New("foreign key constraint violated")
}
return nil
}
Loading

0 comments on commit a975b8d

Please sign in to comment.