Loading...
Loading...
Runs the forward-only Drift/SQLite schema-migration ritual — the most dangerous deterministic operation in an offline-first app: a bad migration silently destroys on-device rows that exist nowhere else. Enforces the exact ordered sequence: take a pre-migration file snapshot before the database is opened, bump schemaVersion by exactly one, write an append-only stepByStep forward step (never edit a shipped step, never write a down migration), commit the drift_dev make-migrations schema snapshot, regenerate, then prove it with tests covering every from→to path (incl. multi-version jumps), a write-at-v(n)/read-at-v(n+1) content test, PRAGMA integrity_check + foreign_key_check, and a forced mid-migration throw that restores the snapshot. Manual, side-effecting workflow. Use when adding or altering a Drift table, column, index, or CHECK; bumping schemaVersion; editing onUpgrade/stepByStep; or writing migration tests.
npx skill4agent add zakariaf/flutter-skills run-migrationAppDatabaseonUpgradePRAGMA wal_checkpoint(TRUNCATE)-wal-wal-shmschemaVersionschemaVersionfromNToMdrift_dev make-migrationsschema dumpdrift_schemas/schema_versions.dartonUpgrademigrateAndValidateCREATEfrom → toPRAGMA integrity_check == okPRAGMA foreign_key_checkpersistence-driftschemaVersiondart run drift_dev make-migrationsmake-migrationsfromNToMrun-codegendart run build_runner build --delete-conflicting-outputsschemaVersiononUpgrade// ── composition boundary (app_database_opener.dart) ─────────────────────────
// Snapshot BEFORE the open, restore AFTER a failed open — no connection is ever
// live while the bytes are copied. Opening is what triggers onUpgrade.
Future<AppDatabase> openMigratedDatabase(File dbFile) async {
// 1. Checkpoint any prior handle, then copy dbFile + its -wal/-shm sidecars.
final snapshot = await _snapshotDbFiles(dbFile);
final db = AppDatabase(NativeDatabase(dbFile));
try {
// 2. Force the migration to run NOW, while the file fallback still stands.
await db.customStatement('PRAGMA user_version;');
return db;
} catch (_) {
// 3. The failed connection is dead — close it, THEN restore with nothing open.
await db.close();
await _restoreDbFiles(snapshot);
rethrow;
}
}
// ── app_database.dart ───────────────────────────────────────────────────────
int get schemaVersion => 3; // bump by exactly ONE per shipped schema change
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
// FKs OFF here — BEFORE the migration's own transaction opens, because
// the pragma is ignored once a transaction is running.
await customStatement('PRAGMA foreign_keys = OFF;');
// Forward-only, append-only stepwise migration in one transaction.
await transaction(() => _steps(m, from, to));
// Prove the body left no dangling references — in production, not tests.
final orphans = await customSelect('PRAGMA foreign_key_check').get();
if (orphans.isNotEmpty) {
throw StateError('foreign_key_check found ${orphans.length} orphans');
}
},
// Pragmas are per-connection, NOT persisted — re-assert on every open.
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON;');
},
);
final _steps = stepByStep(
from1To2: (m, schema) async { /* shipped — DO NOT EDIT */ },
from2To3: (m, schema) async {
// Create EXACTLY the objects this version adds.
await m.addColumn(schema.notes, schema.notes.archivedAtUtc);
},
);make-migrationsschemaVersion# 1. Export the schema for the CURRENT schemaVersion.
dart run drift_dev schema dump lib/data/app_database.dart drift_schemas/
# 2. Era-correct classes. --data-classes --companions is LOAD-BEARING: it emits
# DatabaseAtV1 / DatabaseAtV2 with era-correct data classes and companions,
# the only mechanism that lets a test write rows in the v1 world and read
# them back in the v2 world. Without it only shape tests are possible.
dart run drift_dev schema generate --data-classes --companions \
drift_schemas/ test/drift/generated/
# 3. Step-by-step helpers — each callback gets a schema frozen at its version.
dart run drift_dev schema steps drift_schemas/ lib/data/schema_versions.dartv1 → v3group('schema shape', () {
for (var from = 1; from < kLatestSchemaVersion; from++) {
for (var to = from + 1; to <= kLatestSchemaVersion; to++) {
test('v$from -> v$to', () async {
final connection = await verifier.startAt(from);
final db = AppDatabase(connection);
await verifier.migrateAndValidate(db, to); // shape only — necessary, not sufficient
await db.close();
});
}
}
});columnTransformerINSERT ... SELECT'test1'reason:expecttest('v2 -> v3 preserves every note, byte for byte', () async {
final schema = await verifier.schemaAt(2);
// Write real data with v2-era classes. newConnection() over the SAME bytes is
// what lets different-era database objects see one database.
final old = v2.DatabaseAtV2(schema.newConnection());
await old.into(old.notes).insert(
v2.NotesCompanion.insert(title: 'Blank-ish', body: const Value(' ')),
);
await old.close();
// Run the REAL migration, then prove integrity — FKs are off during migration
// and SQLite reports a dangling reference to nobody.
final db = AppDatabase(schema.newConnection());
await verifier.migrateAndValidate(db, 3);
expect(await db.customSelect('PRAGMA integrity_check').get(),
[predicate((QueryRow r) => r.data.values.first == 'ok')]);
expect(await db.customSelect('PRAGMA foreign_key_check').get(), isEmpty,
reason: 'migration left dangling references — a lost record');
await db.close();
// Read back with v3-era classes and assert CONTENT, not just shape.
final now = v3.DatabaseAtV3(schema.newConnection());
final rows = await now.select(now.notes).get();
expect(rows, hasLength(1), reason: 'a row disappeared during migration');
expect(rows.single.body, ' ', reason: 'whitespace-only body must survive intact');
await now.close();
});PRAGMA foreign_keysonUpgradetransaction()beforeOpenPRAGMA foreign_key_checkflutter testsqlite3_flutter_libslibsqlite3libsqlite3-devschemaVersionmigrateAndValidateINSERT ... SELECTcolumnTransformer'test1'selectupdateforeign_keyssynchronouseraseDatabaseOnSchemaChangeschemaVersionfromNToMmake-migrationsschemaschema_versions.dart--delete-conflicting-outputskLatestSchemaVersionreason:expectPRAGMA integrity_check == okPRAGMA foreign_key_checkbeforeOpeneraseDatabaseOnSchemaChangemake-migrationsdrift_schemas/schema_versions.dartpersistence-driftrun-codegenbuild_runnererror-handling-typed-resultstesting-strategyci-pipeline-and-gatesSchemaVerifiermake-migrationsstepByStepPRAGMA integrity_checkforeign_key_check