import 'package:drift/drift.dart'; import 'enums.dart'; import 'tables.dart'; part 'database.g.dart'; /// The encrypted local inventory database (SQLCipher via an injected executor). /// /// The executor is passed in so production wires SQLCipher while tests can use /// an in-memory database — the schema and queries are identical either way. @DriftDatabase( tables: [ Varieties, VarietyVernacularNames, Species, SpeciesCommonNames, Lots, GerminationTests, ConditionChecks, Movements, Parties, Attachments, ExternalLinks, ], ) class AppDatabase extends _$AppDatabase { AppDatabase(super.e); /// Current schema version; also stamped into interchange exports so an /// importer knows which app generation wrote the file (data-model §7). static const int currentSchemaVersion = 8; @override int get schemaVersion => currentSchemaVersion; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) async => m.createAll(), onUpgrade: (m, from, to) async { // v2: lots can hold seeds or living plant material. if (from < 2) { await m.addColumn(lots, lots.type); } // v3: optional harvest month alongside the harvest year. if (from < 3) { await m.addColumn(lots, lots.harvestMonth); } // v4: optional packaging/supply form for living lots. if (from < 4) { await m.addColumn(lots, lots.presentation); } // v5: display order for attachments (cover photo = lowest sortOrder). if (from < 5) { await m.addColumn(attachments, attachments.sortOrder); } // v6: photo-first draft varieties awaiting a name ("to catalogue" tray). if (from < 6) { await m.addColumn(varieties, varieties.isDraft); } // v7: organic ("eco") self-declaration on varieties; per-species seed // viability (years) reference data for expiry warnings. Guarded by a // column-existence check so a dev database left half-migrated (column // added but user_version not bumped) re-runs cleanly instead of failing // on "duplicate column". if (from < 7) { if (!await _hasColumn('varieties', 'is_organic')) { await m.addColumn(varieties, varieties.isOrganic); } if (!await _hasColumn('species', 'viability_years')) { await m.addColumn(species, species.viabilityYears); } } // v8: absorbs fields from a real seed-bank inventory. Varieties gain a // "needs reproducing" intent and an advisory crop calendar; Lots gain // provenance (origin name/place), a qualitative abundance level and a // preservation format; a new ConditionChecks table logs container count + // drying-agent state. Every step is guarded so a half-migrated dev // database re-runs cleanly (see the v7 note above). if (from < 8) { Future addIfMissing( String table, String column, GeneratedColumn definition, TableInfo tableInfo, ) async { if (!await _hasColumn(table, column)) { await m.addColumn(tableInfo, definition); } } await addIfMissing( 'varieties', 'needs_reproduction', varieties.needsReproduction, varieties, ); await addIfMissing( 'varieties', 'sow_months', varieties.sowMonths, varieties, ); await addIfMissing( 'varieties', 'transplant_months', varieties.transplantMonths, varieties, ); await addIfMissing( 'varieties', 'flowering_months', varieties.floweringMonths, varieties, ); await addIfMissing( 'varieties', 'fruiting_months', varieties.fruitingMonths, varieties, ); await addIfMissing( 'varieties', 'seed_harvest_months', varieties.seedHarvestMonths, varieties, ); await addIfMissing('lots', 'origin_name', lots.originName, lots); await addIfMissing('lots', 'origin_place', lots.originPlace, lots); await addIfMissing('lots', 'abundance', lots.abundance, lots); await addIfMissing( 'lots', 'preservation_format', lots.preservationFormat, lots, ); if (!await _hasTable('condition_checks')) { await m.createTable(conditionChecks); } } }, ); /// Whether a table named [table] already exists. Keeps the additive v8 /// table creation idempotent against partially-migrated databases. Future _hasTable(String table) async { final rows = await customSelect( "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", variables: [Variable.withString(table)], ).get(); return rows.isNotEmpty; } /// Whether [table] already has a column named [column]. Used to keep additive /// migrations idempotent against partially-migrated databases. Future _hasColumn(String table, String column) async { final rows = await customSelect( 'SELECT 1 FROM pragma_table_info(?) WHERE name = ?', variables: [Variable.withString(table), Variable.withString(column)], ).get(); return rows.isNotEmpty; } }