Database Migration Checklist
This document provides a checklist for developers and reviewers to ensure that database migrations are implemented correctly, safely, and consistently across the project. Following these steps is mandatory for any change that alters the database schema.
Phase 1: Development
The developer implementing the migration is responsible for completing these steps.
1. Schema and Versioning:
- Bump the Database Version: The
DB_VERSIONconstant inlegacy/storage/src/main/java/com/fsck/k9/storage/StoreSchemaDefinition.javahas been incremented by exactly 1. - Update the database schema definition: Any changes to the database schema have been reflected in the
dbCreateDatabaseFromScratchwithin theStoreSchemaDefinition.javafile. - Create a New Migration Class: A new
MigrationToXX.ktclass file within thelegacy/storage/src/main/java/com/fsck/k9/storage/migrationsfolder has been created, whereXXis the new database version number. - Migration Logic Implemented: The new migration class contains the necessary SQL statements to transition the database from the previous version to the new version.
- Register the Migration: The new migration class has been registered in the
Migrations.ktfile in thelegacy/storage/src/main/java/com/fsck/k9/storage/migrationsfolder.
2. Migration Logic:
- Data Integrity: The migration logic has been designed to preserve existing data and ensure data integrity.
- Idempotency: The migration can be safely re-run without causing issues or data corruption.
- Error Handling: Appropriate error handling has been implemented to manage potential issues during the migration process.
- No Network Calls: The migration does not make any network calls. If network calls are absolutely necessary, they are handled gracefully and do not fail the migration.
- Self-contained Logic: The migration logic is self-contained within the migration class and does not depend on application logic outside of it.
- Performance Considerations: Long running migrations will block the app startup (a dedicated loading screen will be shown). Consider breaking them into smaller steps if necessary.
- Documentation: The migration class includes comments explaining the purpose of the migration and any non-obvious logic.
3. Testing:
- Unit Tests: Unit tests for the migration have been written to cover various scenarios, including edge cases.
- Test Schema Changes: The test validates that the schema is correct after the migration runs. It should check for:
- New tables exist.
- New columns exist in the correct tables.
- Correct column types, nullability, and default values.
- Test Data Migration: The test validates that existing data is correctly migrated. This includes:
- Data in existing tables remains intact.
- Data transformations (if any) are correctly applied.
4. Holistic testing:
- Fresh Install: The app installs and runs correctly on a fresh install.
- Upgrade from Production: The app upgrades correctly from the latest production version.
- Upgrade from Beta: The app upgrades correctly from the latest beta version.
Phase 2: Review
The reviewer is responsible for validating these steps during the code review process.
1. Code and Logic Review:
- Verify Version Bump: Confirm that the database version has been incremented correctly by 1.
- Schema Definition Update: Ensure that the database schema definition has been updated to reflect the new schema.
- Review Migration Class: Ensure the new migration class is correctly named and placed in the appropriate directory.
- Validate Migration Logic: Review the SQL statements in the migrate() method for correctness and safety.
- Check for Data Integrity: Ensure that the migration logic preserves existing data and does not introduce data loss unless explicitly intended.
- Performance Review: Assess the migration logic for potential performance bottlenecks, especially on large datasets.
- Review Documentation: Check that the migration class is well-commented, explaining the purpose and any complex logic.
2. Testing Review:
- Confirm Unit Tests: Ensure that unit tests for the migration have been written and cover various scenarios.
- Review Test Coverage: Validate that the tests adequately cover the schema changes and data migration paths, including edge cases.
- Review Reordering Scenarios: If the PR involves reordering, confirm that the developer has followed the renumbering protocol and re-verified their changes.
3. Holistic check:
- Fresh Install: The app installs and runs correctly on a fresh install.
- Upgrade from Production: The app upgrades correctly from the latest production version.
- Upgrade from Beta: The app upgrades correctly from the latest beta version.
What to watch out for:
- Data Loss: Ensure that no unintended data loss occurs during the migration.
- Network Calls: Avoid making network calls during the migration process. If necessary, ensure they are handled gracefully and do not fail the migration.
- Merge Conflicts on Uplift: Be prepared for merge conflicts in
StoreSchemaDefinition.javaandMigrations.ktwhen rebasing. When resolving them, ensure you are correctly renumbering your migration and not overwriting someone else’s. - Uplifting Hotfixes: When uplifting a hotfix with a migration to a public branch (
beta,release), its version number must be higher than the highest version across all branches (main,beta,release). See “Scenario B” for the detailed “jump over” strategy. Never rewrite the migration history of a public branch. - Write migrations that are self-contained and do not depend on application logic outside the migration class.
- Long-running Migrations: Be cautious of migrations that may take a long time to complete, especially on large datasets. Consider breaking them into smaller steps if necessary.
- Blocking the Main Thread: Migrations run on the main thread and will block the UI. Keep migrations as fast as possible.
Handling Rebases and Uplifts
When working with migrations, you’ll often encounter situations where the order changes. Below are two common scenarios and how to handle them.
Scenario A: Rebasing a Feature Branch
This happens when you are about to merge your branch, but another migration has been merged into main in the meantime.
Step 1: Renumber Your Migration
- Rebase Your Branch: Before merging, always rebase your branch on the latest version of
main. - Resolve Conflicts: If another migration has taken your version number, you’ll need to renumber your migration to the next available version number.
- Rename and Update Files:
- Rename your
MigrationToXX.ktfile toMigrationToYY.kt, whereYYis the new, higher version number. - Update the class name inside the file to match (e.g.,
class MigrationToYY(...)). - Update the
DB_VERSIONinStoreSchemaDefinition.javatoYY. - Update the registration in
Migrations.ktto use your newMigrationToYYclass.
- Rename your
Step 2: Verify Your Renumbered Migration
Renumbering is changing the context of your migration, and you must verify its correctness again.
- Verify Schema Definition: Ensure that the
dbCreateDatabaseFromScratchmethod inStoreSchemaDefinition.javareflects the correct final schema after all migrations, including your renumbered one. - Verify Migration Logic: Your migration will now run after a different one. Review your SQL logic to ensure it’s still valid. For example, if you are modifying a table that the new intermediate migration also touched, you must ensure your changes don’t cause conflicts.
- Re-run All Tests: Thoroughly re-run all unit and integration tests to ensure your migration still works as expected in the new order.
Scenario B: Uplifting a Hotfix to a Public Branch (beta or release)
This scenario is complex and requires extreme care. It occurs when a hotfix with a migration needs to be applied to
release and/or beta, while newer migrations may already exist on beta.
The goal is to release a hotfix without breaking any user’s upgrade path, regardless of which track (release/beta) they are on or switch to later.
What you must preserve
- Always Upgrade: A user’s database version must only ever increase. No downgrades are allowed.
- Public History is Immutable: Never rewrite the public migration history once a migration has shipped in a public build (
beta,release), you must not renumber, remove, or alter it. - Minimize Hotfix Migrations: Avoid migrations in hotfixes if possible. If unavoidable, keep them minimal and fully self-contained.
A “How to uplift” guide
Warning: This is a delicate process and requires careful attention to detail.
- Audit current versions:
R= latest version shipped onreleaseB= latest version shipped onbetaM= current version onmain(where your hotfix branch is based)
- Pick the hotfix version:
- Set the hotfix migration version
Htomax(R, B, M) + 1(pick +2/+3 if multiple hotfixes are needed).
- Set the hotfix migration version
- Create the hotfix migration on the target branch (
betaorrelease):- Create
MigrationToH.ktwith your migration logic. - Update
DB_VERSIONinStoreSchemaDefinition.javatoH. - Register the migration in
Migrations.kt. - Update the definition in
dbCreateDatabaseFromScratchto the postHschema.
- Create
- Patch
betafirst, thenmainimmediately after:- Set
DB_VERSIONinStoreSchemaDefinition.javaonbeta,maintoH. - Register
MigrationToHinMigrations.kton both branches. Reuse the same migration. - Verify
dbCreateDatabaseFromScratchon both branches reflects the post-Hschema.
- Set
- Reconcile any unreleased migrations below
H:- For both
betaandmain, ensure any unreleased migrations with version< Hare updated against the new schema afterH. - This may involve reintroducing them as new migrations with versions
H+1,H+2, etc., on both branches. - Verify
dbCreateDatabaseFromScratchagain to ensure it reflects the final schema after all migrations.
- For both
Example
Scenario: Uplift a hotfix migration MigrationTo9.kt from main to beta, while preserving existing migrations.
Initial state before uplift:
releasebranch is at versionR=5. The last shipped migration isMigrationTo5.ktbetabranch is at versionB=7. This includesMigrationTo6.ktandMigrationTo7.kt.- Let’s assume a beta build with version
7has not yet been shipped. But a version6has been shipped to beta users.
- Let’s assume a beta build with version
mainbranch is at versionM=10, it contains migrations up toMigrationTo10.kt.- A critical bug requires a hotfix. The necessary migration logic currently exists on main as
MigrationTo9.kt.
Goal: Release the logic from MigrationTo9.kt as a hotfix to beta users without disrupting the unreleased MigrationTo7.kt.
Steps:
- Pick hotfix version ->
H=11. - Create the Hotfix on a temporary branch based on
beta:- On your new branch, create
MigrationTo11.kt. Copy the logic from main’sMigrationTo9.ktinto this new file. - Review the logic, since it will now run after
MigrationTo7.kt. - Update
DB_VERSIONinStoreSchemaDefinition.javato11. - Register
MigrationTo11.ktinMigrations.kt. - Update
dbCreateDatabaseFromScratchto include the schema changes fromMigrationTo7.ktandMigrationTo11.kt. - Merge the hotfix to
beta, which is now on version11.
- On your new branch, create
- Reconcile
main:- The
mainbranch has a more complex history (MigrationTo8.kt,MigrationTo9.kt,MigrationTo10.kt) that must be re-evaluated now that version11has been introduced. - The original
MigrationTo9.kton main is now redundant and its version number is obsolete. - Create a PR for
mainto:- Remove obsolete
MigrationTo9.kt. - Add the same
MigrationTo11.ktfile that was used on thebetabranch and register it inMigrations.kt. - Renumber and re-introduce other migrations (
MigrationTo8.ktasMigrationTo12.kt,MigrationTo10.ktasMigrationTo13.kt) to ensure continuity. - Set the final
DB_VERSIONinStoreSchemaDefinition.javato13. - Update
dbCreateDatabaseFromScratchto reflect the final schema after all migrations.
- Remove obsolete
- The
Result:
releasebranch remains at version5.betaships version11.mainbranch is now at version13.- All user upgrade paths are preserved:
- Release users on version 5 will not be affected by this hotfix. They will only upgrade when a new version is published to the release track.
- Beta users on the pre-hotfix version
6will upgrade directly to version11, correctly applying migrations7and11.
- The migration history is now linear and consistent across all branches, preventing future conflicts.