Skip to content

store/migrations: add per-migration version column so re-shaped migrations don't need _v2 suffixes #316

@wesm

Description

@wesm

Context

Surfaced during the design re-read of PR #304. Not a blocker — there's only one migration today (legacy_identity_to_per_account) and nothing has gone wrong. Filing because the design that's about to grow has a known limitation worth recording before a second migration adds a second case.

The limitation

internal/store/migrations.go records applied migrations as:

CREATE TABLE IF NOT EXISTS applied_migrations (
    name        TEXT PRIMARY KEY,
    applied_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

MarkMigrationApplied is INSERT OR IGNORE keyed on name. The contract is "this named migration has run at least once." It can't express "this named migration has run with this implementation."

Real cases this will surface:

  • The legacy_identity_to_per_account migration was extended to skip non-email source types (bcdde74) and defer when no eligible sources exist (ca2afac). If those changes had needed to re-process sources that were skipped under the original implementation, there would have been no way to distinguish "migration ran, didn't apply to this source" from "migration ran fully."
  • A future migration that broadens its eligibility filter (add apple-mail to the email-source allowlist, say) needs a fresh sentinel name. The natural name (legacy_identity_to_per_account_v2) bakes a version into the migration identifier — workable but ugly.
  • Migrations that are idempotent except for one edge case have no clean way to opt back in.

Why it matters

Most projects hit this somewhere between migration #2 and migration #5. The longer it goes unaddressed, the more migrations accumulate _v2 / _v3 suffix conventions that drift apart.

Proposed approach

Add a version INTEGER NOT NULL DEFAULT 1 column to applied_migrations, and let MarkMigrationApplied accept an optional version. IsMigrationApplied(name, version) becomes SELECT 1 WHERE name = ? AND version >= ?. Existing callers default to version 1, which is the exact semantics they already have.

Cheap, backward compatible, removes the suffix-naming pressure when the second migration's implementation needs a re-run path.

Alternative: introduce a fresh migration_runs history table (one row per run with name/version/applied_at) and keep applied_migrations as a current-state cache. Heavier; pays off if there's ever a "rollback the last migration" story.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions