Files
Oleksandr Bezdieniezhnykh 909f69cb3a [AZ-505] Tile inventory endpoint + HTTP/2 + Leaflet covering index
Production code:
- POST /api/satellite/tiles/inventory (XOR body, 5000-cap,
  most-recent-per-location_hash select, present/absent shaping).
- Kestrel HttpProtocols.Http1AndHttp2 on every listener (AC-5).
- Migration 015 creates tiles_leaflet_path covering index over
  (location_hash, captured_at DESC, updated_at DESC, id DESC)
  INCLUDE (file_path, source); drops superseded idx_tiles_location_hash.
- TileRepository.GetByTileCoordinatesAsync rewired to filter by
  location_hash (Index Only Scan via tiles_leaflet_path).
- TileRepository.GetTilesByLocationHashesAsync added with Npgsql-
  direct ANY($1::uuid[]) binding (Dapper IEnumerable expansion is
  incompatible with the array form).
- Uuidv5.LocationHashForTile centralises the UUIDv5(TileNamespace,
  "{z}/{x}/{y}") formula — single source of truth for the cross-repo
  invariant (gps-denied-onboard parity).

Contracts:
- New: contracts/api/tile-inventory.md v1.0.0.
- Bumped: contracts/data-access/tile-storage.md to v2.0.0 (joint
  ownership by AZ-503-foundation + AZ-505: schema + covering index +
  GetByTileCoordinatesAsync rewrite).

Tests:
- TileInventoryTests covers AC-1, AC-2 (DB-level), AC-4, AC-6.
- Http2MultiplexingTests covers AC-5 (20 concurrent multiplexed GETs
  over h2c via SocketsHttpHandler + AppContext Http2Unencrypted switch).
- LeafletPathIndexOnlyTests covers AC-3 (EXPLAIN (ANALYZE, BUFFERS)
  asserts Index Only Scan over tiles_leaflet_path with heap_blocks=0).

Docs:
- architecture.md, system-flows.md, data_model.md, module-layout.md,
  glossary.md, modules/api_program.md, modules/dataaccess_tile_repository.md,
  components/02_data_access/description.md all updated to reference the
  v2.0.0 tile-storage contract + new tile-inventory contract + AC-7.

Reports:
- batch_01_cycle6_report.md, batch_01_cycle6_review.md,
  implementation_completeness_cycle6_report.md (PASS),
  implementation_report_tile_inventory_cycle6.md.

Task spec moved todo/ -> done/.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:16:37 +03:00

8.3 KiB
Raw Permalink Blame History

DataAccess (Persistence)

1. High-Level Overview

Purpose: Database persistence layer providing Dapper-based repositories for tiles, regions, routes, and route points, plus DbUp-driven schema migrations.

Architectural Pattern: Repository pattern with raw SQL (Dapper)

Upstream dependencies: None at project level (uses Microsoft.Extensions abstractions from NuGet)

Downstream consumers: TileDownloader (TileRepository), RegionProcessing (RegionRepository), RouteManagement (RouteRepository, RegionRepository), WebApi (TileRepository for ServeTile)

2. Internal Interfaces

Interface: ITileRepository

Method Input Output Async Error Types
GetByIdAsync Guid TileEntity? Yes NpgsqlException
GetByTileCoordinatesAsync zoom, x, y TileEntity? (most-recent across sources/flights, AZ-505 rewired to filter on location_hash for Index Only Scan against tiles_leaflet_path; selection rule unchanged from AZ-484) Yes NpgsqlException
GetTilesByRegionAsync lat, lon, sizeM, zoom IEnumerable<TileEntity> (one row per cell via DISTINCT ON, AZ-484) Yes NpgsqlException
GetTilesByLocationHashesAsync IReadOnlyList<Guid> location hashes IReadOnlyDictionary<Guid, TileEntity> (one row per requested hash via DISTINCT ON (location_hash), AZ-505) Yes NpgsqlException
InsertAsync TileEntity Guid (integer-only flight-aware UPSERT, AZ-503-foundation; supersedes the AZ-484 5-column float-based UPSERT) Yes NpgsqlException
UpdateAsync TileEntity int Yes NpgsqlException
DeleteAsync Guid int Yes NpgsqlException

FindExistingTileAsync was removed by AZ-376 (replaced by direct cell lookups through GetByTileCoordinatesAsync + GetTilesByRegionAsync).

GetTilesByLocationHashesAsync is intentionally NOT routed through Dapper. Npgsql binds uuid[] parameters to ANY($1::uuid[]) queries as a single array column, while Dapper's parameter expander rewrites any IEnumerable parameter to a comma-separated list of scalar placeholders, producing ANY((@p0, @p1, ...)) — which is invalid SQL. The method uses NpgsqlCommand with an explicit NpgsqlParameter typed Array | Uuid, and maps results manually from NpgsqlDataReader. This is the documented escape hatch for array-binding hot paths.

Interface: IRegionRepository

Method Input Output Async Error Types
GetByIdAsync Guid RegionEntity? Yes NpgsqlException
GetByStatusAsync string IEnumerable<RegionEntity> Yes NpgsqlException
InsertAsync RegionEntity Guid Yes NpgsqlException
UpdateAsync RegionEntity int Yes NpgsqlException
DeleteAsync Guid int Yes NpgsqlException

Interface: IRouteRepository

Method Input Output Async Error Types
GetByIdAsync Guid RouteEntity? Yes NpgsqlException
GetRoutePointsAsync Guid routeId IEnumerable<RoutePointEntity> Yes NpgsqlException
InsertRouteAsync RouteEntity Guid Yes NpgsqlException
InsertRoutePointsAsync IEnumerable<RoutePointEntity> void Yes NpgsqlException
UpdateRouteAsync RouteEntity int Yes NpgsqlException
LinkRouteToRegionAsync routeId, regionId, isGeofence, polygonIndex void Yes NpgsqlException
GetRegionIdsByRouteAsync Guid routeId IEnumerable<Guid> Yes NpgsqlException
GetGeofenceRegionIdsByRouteAsync Guid routeId IEnumerable<Guid> Yes NpgsqlException
GetGeofenceRegionsByPolygonAsync Guid routeId Dictionary<int, List<Guid>> Yes NpgsqlException
GetRoutesWithPendingMapsAsync IEnumerable<RouteEntity> Yes NpgsqlException

Class: DatabaseMigrator

Method Input Output Async Error Types
RunMigrations bool No Exception

4. Data Access Patterns

Queries

Query Frequency Hot Path Index Needed
GetByTileCoordinatesAsync (tile lookup, leaflet hot path) Very High Yes tiles_leaflet_path covering index — (location_hash, captured_at DESC, updated_at DESC, id DESC) INCLUDE (file_path, source) (AZ-505). Target plan: Index Only Scan with Heap Fetches = 0 after VACUUM ANALYZE.
GetTilesByLocationHashesAsync (bulk inventory, AZ-505) High Yes tiles_leaflet_path leading column. Inventory returns more columns than the INCLUDE list, so a bounded heap fetch is expected; AC-4 budget (≤ 1000 ms p95 / 2500 tiles) absorbs it.
GetTilesByRegionAsync (spatial) High Yes (latitude, longitude, tile_zoom)
InsertAsync (tile per-source-per-flight upsert) High Yes Composite unique on (tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, '00000000-0000-0000-0000-000000000000'::uuid)) (AZ-503-foundation: idx_tiles_unique_identity; supersedes the AZ-484 float-based idx_tiles_unique_location_source)
GetByStatusAsync (region polling) Medium No (status)
GetRoutesWithPendingMapsAsync Low No (request_maps, maps_ready)

Storage Estimates

Table Est. Row Count (1yr) Row Size Growth Rate
tiles ~100K1M (depends on usage) ~200B Variable
regions ~10K50K ~150B Proportional to tile requests
routes ~1K5K ~200B Low
route_points ~50K500K ~100B Proportional to routes
route_regions ~10K100K ~50B Proportional to routes

5. Implementation Details

State Management: Stateless — each repository creates a new Npgsql connection per method call. Npgsql handles internal connection pooling.

Key Dependencies:

Library Version Purpose
Dapper 2.1.35 Micro-ORM for SQL queries
Npgsql 9.0.2 PostgreSQL ADO.NET driver
dbup-postgresql 6.0.3 Schema migration runner

Error Handling: Exceptions propagate to callers. No retry logic at the repository level.

7. Caveats & Edge Cases

  • Repository interfaces are defined in this project (not in Common), creating a dependency from Services to DataAccess
  • Column mapping uses SQL aliases (tile_zoom as TileZoom) rather than Dapper attribute mapping
  • TileRepository.InsertAsync uses an integer-only, flight-aware UPSERT pattern (AZ-503; supersedes the AZ-484 5-column float-based UPSERT). Same-source same-flight re-inserts overwrite and refresh captured_at/location_hash/content_sha256; different sources or different flights at the same cell coexist as separate rows. id is intentionally NOT overwritten on conflict so it stays deterministic per AZ-503 AC-2.
  • TileEntity.Source is stored as a plain string (not the TileSource enum) due to Dapper issue #259 — see _docs/LESSONS.md L-001. Conversion happens via SatelliteProvider.Common.Enums.TileSourceConverter
  • AZ-503 deterministic identity: id is Uuidv5(TileNamespace, "{z}/{x}/{y}/{source}/{flight_id or zero-uuid}") and location_hash is Uuidv5(TileNamespace, "{z}/{x}/{y}"). The cross-repo TileNamespace constant lives in SatelliteProvider.Common.Utils.Uuidv5 and MUST match gps-denied-onboard/components/c6_tile_cache/_uuid.py:TILE_NAMESPACE.
  • The tile-storage contract (_docs/02_document/contracts/data-access/tile-storage.md) was bumped to v2.0.0 jointly by AZ-503-foundation (identity columns + integer UPSERT, cycle 5) and AZ-505 (covering index tiles_leaflet_path + location_hash-keyed reads + bulk inventory, cycle 6). The frozen v2.0.0 spec is the authoritative read-side / write-side / index contract for external consumers; the per-method shape table above mirrors it for in-component readers.
  • No soft-delete; DeleteAsync is a hard delete

8. Dependency Graph

Must be implemented after: nothing (parallel with Common) Can be implemented in parallel with: Common Blocks: TileDownloader, RegionProcessing, RouteManagement, WebApi

9. Logging Strategy

Log Level When Example
INFO Migration start/complete Starting database migrations...
ERROR Migration failure Database migration failed

Structured logging via ILogger<T>. Logger injected but rarely used in repositories.