using LinqToDB.Data; namespace Azaion.Missions.Database; // Forward-only migrator. Two SQL blocks run on every container start, in order: // // 1. RenameAndDropSql -- idempotent ALTERs that bring a legacy `flights`-era // schema up to the renamed `missions` schema, plus DROPs for the // GPS-Denied tables (Jira AZ-EPIC B7 / B9). On a fresh DB or an // already-migrated DB this block is a no-op (every statement guards on // IF EXISTS / column existence). // // 2. InitSql -- CREATE TABLE IF NOT EXISTS for the post-migration schema. // On a legacy DB the renames in (1) leave tables already present; this // block is a no-op for them. On a fresh-install device this block IS // the schema. On an already-migrated DB it is a no-op. // // Re-running the migrator on any DB state above produces no errors and no // changes -- this is the suite's "idempotent forward-only" convention. public static class DatabaseMigrator { public static void Migrate(AppDataConnection db) { db.Execute(RenameAndDropSql); db.Execute(InitSql); } // Bring legacy `flights` / `aircrafts` schemas up to the renamed shape. // Safe to re-run on any DB state. private const string RenameAndDropSql = """ ALTER TABLE IF EXISTS aircrafts RENAME TO vehicles; ALTER TABLE IF EXISTS flights RENAME TO missions; DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'missions' AND column_name = 'aircraft_id') THEN ALTER TABLE missions RENAME COLUMN aircraft_id TO vehicle_id; END IF; IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'waypoints' AND column_name = 'flight_id') THEN ALTER TABLE waypoints RENAME COLUMN flight_id TO mission_id; END IF; IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'map_objects' AND column_name = 'flight_id') THEN ALTER TABLE map_objects RENAME COLUMN flight_id TO mission_id; END IF; END $$; ALTER INDEX IF EXISTS ix_flights_aircraft_id RENAME TO ix_missions_vehicle_id; ALTER INDEX IF EXISTS ix_waypoints_flight_id RENAME TO ix_waypoints_mission_id; ALTER INDEX IF EXISTS ix_map_objects_flight_id RENAME TO ix_map_objects_mission_id; DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections; """; // Post-migration schema. CREATE TABLE IF NOT EXISTS is the idempotent path // for fresh DBs; on already-migrated DBs every statement here is a no-op. private const string InitSql = """ CREATE TABLE IF NOT EXISTS vehicles ( id UUID PRIMARY KEY, type INTEGER NOT NULL DEFAULT 0, model TEXT NOT NULL, name TEXT NOT NULL, fuel_type INTEGER NOT NULL DEFAULT 0, battery_capacity NUMERIC NOT NULL DEFAULT 0, engine_consumption NUMERIC NOT NULL DEFAULT 0, engine_consumption_idle NUMERIC NOT NULL DEFAULT 0, is_default BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS missions ( id UUID PRIMARY KEY, created_date TIMESTAMP NOT NULL DEFAULT NOW(), name TEXT NOT NULL, vehicle_id UUID NOT NULL REFERENCES vehicles(id) ); CREATE TABLE IF NOT EXISTS waypoints ( id UUID PRIMARY KEY, mission_id UUID NOT NULL REFERENCES missions(id), lat NUMERIC, lon NUMERIC, mgrs TEXT, waypoint_source INTEGER NOT NULL DEFAULT 0, waypoint_objective INTEGER NOT NULL DEFAULT 0, order_num INTEGER NOT NULL DEFAULT 0, height NUMERIC NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS map_objects ( id UUID PRIMARY KEY, mission_id UUID NOT NULL REFERENCES missions(id), h3_index TEXT NOT NULL, mgrs TEXT NOT NULL, lat NUMERIC, lon NUMERIC, class_num INTEGER NOT NULL DEFAULT 0, label TEXT NOT NULL DEFAULT '', size_width_m NUMERIC NOT NULL DEFAULT 0, size_length_m NUMERIC NOT NULL DEFAULT 0, confidence NUMERIC NOT NULL DEFAULT 0, object_status INTEGER NOT NULL DEFAULT 0, first_seen_at TIMESTAMP NOT NULL DEFAULT NOW(), last_seen_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_missions_vehicle_id ON missions(vehicle_id); CREATE INDEX IF NOT EXISTS ix_waypoints_mission_id ON waypoints(mission_id); CREATE INDEX IF NOT EXISTS ix_map_objects_mission_id ON map_objects(mission_id); -- B12 (Option A): exactly-one-default vehicle is enforced by a partial -- unique index. Only rows with is_default = true are indexed; two such -- rows would conflict. Existing 0-default and 1-default DBs are valid. CREATE UNIQUE INDEX IF NOT EXISTS ux_vehicles_one_default ON vehicles (is_default) WHERE is_default = TRUE; """; }