# 04 — Persistence (Edge PostgreSQL) **Spec source**: `../../../suite/_docs/00_database_schema.md` (authoritative ER diagram), `../../../suite/_docs/00_top_level_architecture.md` § Database Topology (per-edge-device PostgreSQL pattern). **Implementation status**: ✅ implemented -- the 4 owned tables migrate cleanly; the 3 borrowed tables read/delete cleanly under standard edge deployment. > **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's source still has 6 owned tables (incl. `orthophotos`, `gps_corrections`) and 9 entity files (incl. `Aircraft.cs`, `Flight.cs`, `Orthophoto.cs`, `GpsCorrection.cs`). Renames + table drops tracked under Jira AZ-EPIC children B5 (namespace), B6 (rename), B7 (GPS-Denied removal), B9 (DB migration). **Files** (post-rename): - Connection / migrator: `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs` - Owned-table entities (4): `Database/Entities/{Vehicle, Mission, Waypoint, MapObject}.cs` - Borrowed-table entities (3): `Database/Entities/{Media, Annotation, Detection}.cs` - Cross-cutting enum: `Enums/ObjectStatus.cs` ## 1. High-Level Overview **Purpose**: All PostgreSQL access for this service against the **shared local edge PostgreSQL**. Owns the LinqToDB `DataConnection` (one per HTTP request), the entity row maps, and the in-process schema bootstrap. The shared-DB pattern is documented in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology -- every edge service connects to the same `postgres-local` and migrates only its own tables. **Architectural pattern**: linq2db `DataConnection` + attribute-mapped entities. No repository abstraction. **Upstream dependencies**: None. **Downstream consumers**: `01_vehicle_catalog` (`VehicleService`), `02_mission_planning` (`MissionService`, `WaypointService`), `07_host` (registers the connection + runs the migrator). ## 2. Internal Interface ```csharp public class AppDataConnection(DataOptions options) : DataConnection(options) { // Owned tables -- schema migrated by this service ITable Vehicles; // owned + written ITable Missions; // owned + written ITable Waypoints; // owned + written ITable MapObjects; // owned schema; written by autopilot // Borrowed tables -- schema migrated by other suite services ITable Media; // owned by `annotations` service ITable Annotations; // owned by `annotations` service ITable Detections; // owned by detection pipeline } public static class DatabaseMigrator { static void Migrate(AppDataConnection db); } ``` Entity surface -- see `modules/entities.md` for column-level shape. ## 3. External API Not applicable. ## 4. Data Access Patterns ### Tables this service migrates (owned) | Table | Schema source | Writers | Indexes | |-------|---------------|---------|---------| | `vehicles` | this migrator | `01_vehicle_catalog` | PK only | | `missions` | this migrator | `02_mission_planning` | PK + `ix_missions_vehicle_id` | | `waypoints` | this migrator | `02_mission_planning` | PK + `ix_waypoints_mission_id` | | `map_objects` | this migrator | `autopilot` (per `../../../suite/_docs/06_autopilot_design.md`) | PK + `ix_map_objects_mission_id` | **Removed in B7 + B9**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service. `DatabaseMigrator` includes a one-shot `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;` for fielded edge devices that previously ran the legacy schema (B9). ### Tables this service borrows (NOT migrated here, intentional cross-service ownership) | Table | Schema source | Writers | This service's interaction | |-------|---------------|---------|----------------------------| | `media` | `annotations` migrator | `annotations` (Media CRUD) | Read `id`, `waypoint_id`; cascade-delete only (during mission/waypoint delete) | | `annotations` | `annotations` migrator | `annotations` (Annotations CRUD) | Read `id`, `media_id`; cascade-delete only | | `detection` (singular) | detection pipeline migrator | `detections` / `ai-training` | Read `id`, `annotation_id`; cascade-delete only | This split matches the suite-wide pattern in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology and `../../../suite/_docs/01_annotations.md` § Database. Each edge service migrates its own tables; all services see the full shared schema through their own `DataConnection`. ### Caching Strategy None. ### Storage Estimates Not specified in spec. ### Data Management - **Seed data**: none. The migrator only creates schema. - **Rollback**: not built-in. Forward-only-additive (`IF NOT EXISTS`); the B9 DROPs are the one explicit destructive step. ## 5. Implementation Details **State Management**: `DataConnection` is per-HTTP-request scoped (registered via `AddScoped` in `07_host`). Each request gets its own physical Npgsql connection from the pool. All services within one request share that connection. **Algorithmic Complexity**: Trivial -- direct column selects, FK joins via `[Association]`, single-table inserts/updates/deletes. **Key Dependencies**: | Library | Version | Purpose | |---------|---------|---------| | `linq2db` | 6.2.0 | LINQ -> SQL provider, attribute mapping, async extensions | | `Npgsql` | 10.0.2 | PostgreSQL driver | **Error Handling**: linq2db / Npgsql exceptions propagate up; if they happen to be `KeyNotFoundException` / `Argument...` (rare), the global middleware in `06_http_conventions` maps them. Otherwise -> 500. ## 6. Extensions and Helpers None. ## 7. Caveats & Edge Cases 1. **No schema versioning** -- additive `IF NOT EXISTS` only. Column drops, type changes, constraint changes require manual SQL or a migration tool. Acceptable today; will become a problem when the schema evolves under a deployed fleet. The B9 `DROP` is the one explicit exception. 2. **No transaction wrapping in `Migrate`** -- multi-statement `Execute` runs as autocommit-per-statement. All statements are individually idempotent so partial failure is recoverable on next startup. 3. **Mixed PK types**: `Guid` for in-house tables, `string` for `media`, `annotations` (XxHash64-based per `../../../suite/_docs/00_database_schema.md`). The TEXT-PK entities are the ones whose IDs are computed from file content, allowing dedup across services. 4. **Geopoint columns split into 3 fields** (`lat`, `lon`, `mgrs`) -- diverges from the spec's single `string GPS` representation. Carry the divergence to verification log. 5. **`detection` table singularity** -- owned by another service; not this service's call to rename. 6. **`LOWER(...)` indexes absent** -- case-insensitive name search is full-table scan. Fine while tables are small. 7. **No SQL logging configured** -- debug LinqToDB issues by enabling `DataConnection.WriteTraceLine` or wrapping the provider; not done today. ## 8. Dependency Graph **Must be implemented after**: nothing internal. **Can be implemented in parallel with**: `05_identity`, `06_http_conventions`. **Blocks**: `01_vehicle_catalog`, `02_mission_planning`, `07_host`. ## 9. Logging Strategy LinqToDB defaults -- no SQL logging configured.