# Satellite Provider — Data Model ## Entity-Relationship Diagram ```mermaid erDiagram TILES { uuid id PK int tile_zoom float latitude float longitude float tile_size_meters int tile_size_pixels varchar image_type varchar maps_version int version varchar source timestamp captured_at varchar file_path int tile_x int tile_y timestamp created_at timestamp updated_at } REGIONS { uuid id PK float latitude float longitude float size_meters int zoom_level varchar status bool stitch_tiles varchar csv_file_path varchar summary_file_path int tiles_downloaded int tiles_reused timestamp created_at timestamp updated_at } ROUTES { uuid id PK varchar name text description float region_size_meters int zoom_level float total_distance_meters int total_points bool request_maps bool maps_ready bool create_tiles_zip varchar tiles_zip_path varchar csv_file_path varchar summary_file_path varchar stitched_image_path timestamp created_at timestamp updated_at } ROUTE_POINTS { uuid id PK uuid route_id FK int sequence_number float latitude float longitude varchar point_type int segment_index float distance_from_previous timestamp created_at } ROUTE_REGIONS { uuid route_id FK uuid region_id FK bool is_geofence int geofence_polygon_index timestamp created_at } ROUTES ||--o{ ROUTE_POINTS : "has many" ROUTES ||--o{ ROUTE_REGIONS : "has many" REGIONS ||--o{ ROUTE_REGIONS : "linked via" ``` ## Tables ### tiles Stores metadata for downloaded satellite imagery tiles. Each tile is a single image at a specific geographic coordinate and zoom level. | Column | Type | Constraints | Description | |--------|------|-------------|-------------| | id | UUID | PK | Unique tile identifier | | tile_zoom | INT | NOT NULL | Google Maps zoom level (1-20) | | latitude | DOUBLE PRECISION | NOT NULL | Center latitude | | longitude | DOUBLE PRECISION | NOT NULL | Center longitude | | tile_size_meters | DOUBLE PRECISION | NOT NULL | Ground coverage in meters | | tile_size_pixels | INT | NOT NULL | Image dimension in pixels | | image_type | VARCHAR(10) | NOT NULL | Image format (e.g., "jpg") | | maps_version | VARCHAR(50) | | Legacy free-form provider tag; post-AZ-373 new rows write NULL. Vestigial post-AZ-484 (column retained for forensics on pre-existing rows; no longer part of any index) | | version | INT | NOT NULL, DEFAULT 2025 | Year-based versioning for cache invalidation. Vestigial post-AZ-484 — removed from the unique key by migration 012 (preparation for AZ-484); column retained nullable for backward compatibility | | source | VARCHAR(32) | NOT NULL, DEFAULT 'google_maps' | AZ-484: producer of the imagery (`'google_maps'`, `'uav'`). Closed value set — see `tile-storage` v1.0.0 contract Inv-5 and `Common.Enums.TileSourceConverter`. Backfilled to `'google_maps'` for all pre-AZ-484 rows by migration 013 | | captured_at | TIMESTAMP | NOT NULL | AZ-484: imagery acquisition timestamp (UTC). Drives most-recent-across-sources selection. Backfilled to `created_at` for pre-AZ-484 rows by migration 013 | | file_path | VARCHAR(500) | NOT NULL | Relative path to stored image. **AZ-488 per-source layout**: `source='google_maps'` rows keep the legacy bucketed/timestamped path emitted by `StorageConfig.GetTileFilePath` (`{TilesDirectory}/{zoom}/{x_bucket}/{y_bucket}/tile_{zoom}_{x}_{y}_{ts}.jpg`). `source='uav'` rows live under `{TilesDirectory}/uav/{zoom}/{x}/{y}.jpg` — see `_docs/02_document/contracts/api/uav-tile-upload.md` v1.0.0. The authoritative source marker is the `source` column; the per-source path is implementation detail that keeps both producers' bytes individually addressable. | | tile_x | INT | NOT NULL | Tile X coordinate (Slippy Map) | | tile_y | INT | NOT NULL | Tile Y coordinate (Slippy Map) | | created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | | updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | **Indexes** (post-AZ-484): - `idx_tiles_unique_location_source` UNIQUE (latitude, longitude, tile_zoom, tile_size_meters, source) — created by migration 013; replaces the pre-AZ-484 4-col `idx_tiles_unique_location` (which itself superseded the legacy 5-col `(…, version)` index dropped by migration 012) - `idx_tiles_coordinates` (tile_zoom, tile_x, tile_y, version) - `idx_tiles_zoom` (tile_zoom) **Selection rule**: `GetByTileCoordinatesAsync` and `GetTilesByRegionAsync` return the most-recent row across sources for any `(latitude, longitude, tile_zoom, tile_size_meters)` cell. Tie-break: `captured_at DESC, updated_at DESC, id DESC`. Region read uses `DISTINCT ON` to enforce one-row-per-cell at the SQL layer. **UPSERT contract**: `INSERT … ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters, source) DO UPDATE` — same-source re-insert refreshes `file_path, tile_x, tile_y, captured_at, updated_at`. Two producers for the same cell coexist as separate rows. ### regions Tracks region download requests and their processing status. | Column | Type | Constraints | Description | |--------|------|-------------|-------------| | id | UUID | PK | Region request identifier | | latitude | DOUBLE PRECISION | NOT NULL | Center latitude | | longitude | DOUBLE PRECISION | NOT NULL | Center longitude | | size_meters | DOUBLE PRECISION | NOT NULL | Square region side length | | zoom_level | INT | NOT NULL | Zoom level for tiles | | status | VARCHAR(20) | NOT NULL | pending / processing / completed / failed | | stitch_tiles | BOOLEAN | NOT NULL, DEFAULT false | Whether to produce stitched image | | csv_file_path | VARCHAR(500) | | Path to tile manifest CSV | | summary_file_path | VARCHAR(500) | | Path to summary text | | tiles_downloaded | INT | DEFAULT 0 | Count of newly downloaded tiles | | tiles_reused | INT | DEFAULT 0 | Count of cache-hit tiles | | created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | | updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | **Indexes**: - `idx_regions_status` (status) ### routes Defines route paths with configuration for map tile generation. | Column | Type | Constraints | Description | |--------|------|-------------|-------------| | id | UUID | PK | Route identifier | | name | VARCHAR(200) | NOT NULL | Human-readable name | | description | TEXT | | Optional description | | region_size_meters | DOUBLE PRECISION | NOT NULL | Size of region per point | | zoom_level | INT | NOT NULL | Zoom level for regions | | total_distance_meters | DOUBLE PRECISION | NOT NULL | Total route length | | total_points | INT | NOT NULL | Total point count (original + interpolated) | | request_maps | BOOLEAN | NOT NULL, DEFAULT false | Whether to generate map tiles | | maps_ready | BOOLEAN | NOT NULL, DEFAULT false | Whether map generation is complete | | create_tiles_zip | BOOLEAN | NOT NULL, DEFAULT false | Whether to produce ZIP archive | | tiles_zip_path | VARCHAR(500) | | Path to output ZIP | | csv_file_path | VARCHAR(500) | | Route-level CSV | | summary_file_path | VARCHAR(500) | | Route-level summary | | stitched_image_path | VARCHAR(500) | | Route-level stitched image | | created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | | updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | ### route_points Stores all points along a route (both original waypoints and interpolated intermediate points). | Column | Type | Constraints | Description | |--------|------|-------------|-------------| | id | UUID | PK | Point identifier | | route_id | UUID | FK → routes.id, CASCADE | Parent route | | sequence_number | INT | NOT NULL, UNIQUE(route_id, seq) | Order along route | | latitude | DOUBLE PRECISION | NOT NULL | Point latitude | | longitude | DOUBLE PRECISION | NOT NULL | Point longitude | | point_type | VARCHAR(20) | NOT NULL | "original" or "intermediate" | | segment_index | INT | NOT NULL | Which segment (between original points) | | distance_from_previous | DOUBLE PRECISION | | Meters from previous point | | created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | **Indexes**: - `idx_route_points_route` (route_id, sequence_number) - `idx_route_points_coords` (latitude, longitude) ### route_regions Junction table linking routes to their generated region requests, with geofence metadata. | Column | Type | Constraints | Description | |--------|------|-------------|-------------| | route_id | UUID | FK → routes.id, CASCADE, PK | | | region_id | UUID | FK → regions.id, CASCADE, PK | | | is_geofence | BOOLEAN | NOT NULL, DEFAULT false | Whether point is inside a geofence | | geofence_polygon_index | INTEGER | | Which polygon (0-based) the point is in | | created_at | TIMESTAMP | NOT NULL, DEFAULT NOW | | **Indexes**: - `idx_route_regions_route` (route_id) - `idx_route_regions_region` (region_id) ## Migration Strategy - **Tool**: DbUp (embedded SQL scripts) - **Execution**: Automatic on application startup (`DatabaseMigrator.Migrate()`) - **Naming**: `NNN_DescriptiveName.sql` (sequential numbering) - **Storage**: Embedded resources in `SatelliteProvider.DataAccess` assembly - **Tracking**: DbUp's internal `schemaversions` table records which scripts have run - **Rollback**: Not supported — forward-only migrations ## Migration History | # | Migration | Purpose | |---|-----------|---------| | 001 | CreateTilesTable | Base tiles table | | 002 | CreateRegionsTable | Region request tracking | | 003 | CreateIndexes | Performance indexes | | 004 | AddVersionColumn | Year-based tile versioning + dedup | | 005 | CreateRoutesTables | Routes, route_points, route_regions | | 006 | AddStitchTilesToRegions | Stitch flag on regions | | 007 | AddRouteMapFields | request_maps, maps_ready, file paths on routes | | 008 | AddGeofenceFlagToRouteRegions | is_geofence flag | | 009 | AddGeofencePolygonIndex | Polygon index tracking | | 010 | AddTilesZipToRoutes | ZIP generation fields | | 011 | AddTileCoordinates | Slippy map X/Y + rename zoom_level → tile_zoom | | 012 | DropTileVersionConstraint | Drops legacy 5-col `(…, version)` unique index; replaces with 4-col `idx_tiles_unique_location` (preparation for AZ-484) | | 013 | AddTileSourceAndCapturedAt | AZ-484: adds `source` (default `'google_maps'`) + `captured_at` columns; backfills both for pre-existing rows; replaces 4-col unique with 5-col `idx_tiles_unique_location_source`. Transactional; idempotent against partial replays |