Skip to content

Automatic pre-migration backup of opal.db #77

Description

@CST-100

Motivation

The startup revision guard (#50) makes upgrades explicitly one-way: once a binary migrates a database, an older binary refuses to serve it. For v1.4.0-beta.2 the release notes had to tell users "copy opal.db somewhere first if you might want to roll back." That should be something OPAL does for them — it owns the database and knows exactly when a migration is about to change it.

This is the standard pattern for local-first apps that own their store (Home Assistant, the *arr family): snapshot immediately before applying migrations, so rollback = old binary + restored file.

Design

  • Trigger: in init_database(), only when the stamped revision differs from head — i.e. only when migrations will actually run. Normal starts are untouched; one backup per upgrade, not per launch.
  • Mechanism: VACUUM INTO (or the sqlite3 backup API), not a file copy — atomic, consistent, and avoids the WAL/-shm sidecar problem. Compacts as a bonus.
  • Naming: backups/opal-pre-<revision>-<ISO8601>.db in the data dir. Keying on the pre-upgrade revision means a failed upgrade retried on next launch won't clobber the good snapshot with a half-migrated one, and the restore procedure is self-describing.
  • Retention: prune to the last N, OPAL_UPGRADE_BACKUPS (default 1). Databases are megabytes; cost is negligible.
  • Skip: demo databases (demo.<name> is throwaway by design) and in-memory engines.
  • Discoverability: the migration log line and the revision-guard error message both point at the backup path, so the rollback story surfaces exactly when someone needs it.

Non-goals

  • Protecting against migrations that succeed but were wrong and noticed days later — that's general backup strategy, not this feature.
  • An opal restore command — maybe later; for now the restore procedure is "replace the file", documented in the log line.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions