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, GardenOutcomes, Movements, Parties, Attachments, ExternalLinks, Plantares, Sales, ], ) 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 = 13; @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); } } // v9: Plantares — reproduction commitments (data-model §2.7). Guarded so a // half-migrated dev database re-runs cleanly (see the v7 note above). if (from < 9) { if (!await _hasTable('plantares')) { await m.createTable(plantares); } } // v10: Sales — recorded seed sales (separate from gift/Plantare). Guarded. if (from < 10) { if (!await _hasTable('sales')) { await m.createTable(sales); } } // v11: asking price on Lots — published with "sell" market offers. // Guarded (see the v7 note above). if (from < 11) { if (!await _hasColumn('lots', 'price_amount')) { await m.addColumn(lots, lots.priceAmount); } if (!await _hasColumn('lots', 'price_currency')) { await m.addColumn(lots, lots.priceCurrency); } } // v12: the bilateral SIGNED Plantare (plantare-bilateral.md) — keys, both // signatures, the shared pledge id/Movement, the handshake state, and the // structured return option. All nullable/defaulted so v1 local rows are // untouched. Guarded (see the v7 note above). if (from < 12) { Future addIfMissing( String column, GeneratedColumn definition, ) async { if (!await _hasColumn('plantares', column)) { await m.addColumn(plantares, definition); } } await addIfMissing('pledge_id', plantares.pledgeId); await addIfMissing('debtor_key', plantares.debtorKey); await addIfMissing('creditor_key', plantares.creditorKey); await addIfMissing('debtor_signature', plantares.debtorSignature); await addIfMissing('creditor_signature', plantares.creditorSignature); await addIfMissing('movement_id', plantares.movementId); await addIfMissing('remote_state', plantares.remoteState); await addIfMissing('return_kind', plantares.returnKind); await addIfMissing('work_hours', plantares.workHours); } // v13: GardenOutcomes — the per-season "how did it do in my garden" // answer (rating + note) captured when a harvest is recorded. Guarded // (see the v7 note above). if (from < 13) { if (!await _hasTable('garden_outcomes')) { await m.createTable(gardenOutcomes); } } }, ); /// 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; } }