mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 14:41:13 +00:00
c646aa93e2
Foundation half of original AZ-503 (split during /autodev step 10 batch 2
on user choice; deferred work moved to AZ-505 with a Blocks link).
Adds deterministic tile identity (UUIDv5 over (z, x, y, source, flight_id))
shared cross-repo with gps-denied-onboard via the pinned TileNamespace
5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c, switches the tiles UPSERT key from
floats to integers with per-flight separation, plumbs FlightId through
UavTileMetadata + handler, and writes UAV evidence to per-flight
on-disk directories so two flights at the same (z, x, y) coexist.
- Common: pure-C# RFC 9562 Uuidv5 (no third-party dep) + FlightId DTO
field; 10 Python-reference unit vectors verify byte parity.
- DataAccess: migration 014 adds flight_id (uuid NULL), location_hash
(uuid NOT NULL, backfilled via session-scoped pg_temp.uuidv5),
content_sha256 (bytea NULL), legacy_id (uuid NULL = preserves
pre-AZ-503 random id one cycle); drops idx_tiles_unique_location_source
(AZ-484) and adds idx_tiles_unique_identity keyed on
(tile_zoom, tile_x, tile_y, tile_size_meters, source,
COALESCE(flight_id, '00000000-...'::uuid)) + idx_tiles_location_hash.
- TileRepository: ColumnList + UPSERT updated; id never updated on
conflict (preserves AC-2 idempotence). UpdateAsync extended.
- Services: TileService and UavTileUploadHandler compute deterministic
Id + LocationHash + ContentSha256 before insert; UAV file path
becomes ./tiles/uav/{flight_id or 'none'}/{z}/{x}/{y}.jpg.
- Tests: Uuidv5Tests (10 reference vectors), UavTileFilePathTests
(per-flight + anonymous paths), UavTileUploadHandlerTests (AC-2,
AC-3, AC-7, AC-11 unit-level), UavUploadTests (AC-3 + AC-4
integration: multi-flight DB coexistence with shared location_hash
+ distinct file_path; float-different lat/lon collapse to 1 row),
MigrationTests (column shape, idx_tiles_unique_identity supersedes
AZ-484 index, deterministic backfill).
- IntegrationTests project references Common to reuse Uuidv5 in raw
SQL seeds.
- AZ-488 MultiSourceCoexistence seed fixed to populate location_hash
(otherwise migration 014's NOT NULL constraint fails).
ACs covered: AC-1, AC-2, AC-3, AC-4, AC-7, AC-8, AC-11.
ACs deferred to AZ-505: AC-5, AC-6, AC-9, AC-10, AC-12.
Co-authored-by: Cursor <cursoragent@cursor.com>
105 lines
5.1 KiB
PL/PgSQL
105 lines
5.1 KiB
PL/PgSQL
-- AZ-503-foundation: deterministic tile identity (UUIDv5) + multi-flight evidence preservation.
|
|
--
|
|
-- Adds four columns to `tiles`:
|
|
-- - flight_id (uuid NULL) — per-UAV-flight identifier. NULL for google_maps and
|
|
-- legacy UAV rows; populated for AZ-503+ UAV uploads.
|
|
-- - location_hash (uuid NOT NULL) — UUIDv5(TILE_NAMESPACE, "{tile_zoom}/{tile_x}/{tile_y}").
|
|
-- Drives leaflet hot-path lookups and future voting layer.
|
|
-- - content_sha256 (bytea NULL) — SHA-256 of the JPEG body at insert time. NULL for legacy
|
|
-- rows (pre-AZ-503), NOT NULL for new rows enforced at the
|
|
-- application layer (TileEntity / repositories). Kept NULL-able
|
|
-- at the column level because the migration cannot read tile
|
|
-- files from disk safely (path may have moved, file may be
|
|
-- gone). Application invariant: SHA-256 only meaningful when
|
|
-- not NULL.
|
|
-- - legacy_id (uuid NULL) — preserves the pre-AZ-503 random id of each row for one
|
|
-- deprecation cycle (per AZ-503 Risk 1). Dropped in a
|
|
-- follow-up migration once external references to legacy
|
|
-- ids are confirmed flushed.
|
|
--
|
|
-- Switches the UPSERT conflict key from (latitude, longitude, tile_zoom, tile_size_meters, source)
|
|
-- to an integer-only key with per-flight separation:
|
|
-- (tile_zoom, tile_x, tile_y, tile_size_meters, source, COALESCE(flight_id, '00000000-...'::uuid))
|
|
-- so two UAV flights uploading the same (z, x, y) cell coexist as distinct rows.
|
|
--
|
|
-- TILE_NAMESPACE is pinned cross-repo at 5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c (matches
|
|
-- SatelliteProvider.Common.Utils.Uuidv5.TileNamespace and gps-denied-onboard
|
|
-- c6_tile_cache/_uuid.py). DO NOT change without updating both sides.
|
|
--
|
|
-- Whole migration runs inside one transaction; partial failure leaves the table without the
|
|
-- new columns rather than half-migrated (per AZ-484 precedent for tile-table migrations).
|
|
|
|
BEGIN;
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
-- Helper: pure-SQL UUIDv5 (SHA-1-based, RFC 9562 §5.5). Used ONLY for the
|
|
-- location_hash backfill below. Application writes compute the same UUIDv5
|
|
-- via SatelliteProvider.Common.Utils.Uuidv5.Create (verified byte-identical
|
|
-- against Python uuid.uuid5 in AZ-503 AC-1).
|
|
CREATE OR REPLACE FUNCTION pg_temp.uuidv5(namespace_uuid uuid, name text) RETURNS uuid AS $$
|
|
DECLARE
|
|
ns_bytes bytea;
|
|
hash bytea;
|
|
b6 int;
|
|
b8 int;
|
|
BEGIN
|
|
-- Namespace UUID as 16 big-endian bytes.
|
|
ns_bytes := decode(replace(namespace_uuid::text, '-', ''), 'hex');
|
|
hash := substring(digest(ns_bytes || convert_to(name, 'UTF8'), 'sha1') from 1 for 16);
|
|
-- Set version = 5 (upper nibble of byte 6).
|
|
b6 := (get_byte(hash, 6) & 15) | 80;
|
|
hash := set_byte(hash, 6, b6);
|
|
-- Set RFC 4122 variant (upper 2 bits of byte 8 = 10).
|
|
b8 := (get_byte(hash, 8) & 63) | 128;
|
|
hash := set_byte(hash, 8, b8);
|
|
RETURN encode(hash, 'hex')::uuid;
|
|
END;
|
|
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
|
|
ALTER TABLE tiles ADD COLUMN IF NOT EXISTS flight_id uuid;
|
|
ALTER TABLE tiles ADD COLUMN IF NOT EXISTS location_hash uuid;
|
|
ALTER TABLE tiles ADD COLUMN IF NOT EXISTS content_sha256 bytea;
|
|
ALTER TABLE tiles ADD COLUMN IF NOT EXISTS legacy_id uuid;
|
|
|
|
-- Preserve the pre-AZ-503 random id under legacy_id for the deprecation window.
|
|
UPDATE tiles
|
|
SET legacy_id = id
|
|
WHERE legacy_id IS NULL;
|
|
|
|
-- Backfill location_hash for every existing row. Deterministic; same algorithm
|
|
-- the application uses for new writes.
|
|
UPDATE tiles
|
|
SET location_hash = pg_temp.uuidv5(
|
|
'5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c'::uuid,
|
|
tile_zoom::text || '/' || tile_x::text || '/' || tile_y::text)
|
|
WHERE location_hash IS NULL;
|
|
|
|
-- location_hash is now populated for every row; promote to NOT NULL.
|
|
ALTER TABLE tiles ALTER COLUMN location_hash SET NOT NULL;
|
|
|
|
-- content_sha256 is intentionally left nullable for legacy rows (the migration cannot
|
|
-- safely re-read tile files: paths may have rotated, files may be absent). The application
|
|
-- layer enforces NOT NULL for all writes starting at AZ-503; legacy NULLs are treated as
|
|
-- "unverified content" and surfaced as such if/when integrity checks are added downstream.
|
|
|
|
-- Drop AZ-484's lat/lon-keyed unique index and replace with the integer + flight_id key.
|
|
DROP INDEX IF EXISTS idx_tiles_unique_location_source;
|
|
DROP INDEX IF EXISTS idx_tiles_unique_location;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_tiles_unique_identity
|
|
ON tiles (
|
|
tile_zoom,
|
|
tile_x,
|
|
tile_y,
|
|
tile_size_meters,
|
|
source,
|
|
COALESCE(flight_id, '00000000-0000-0000-0000-000000000000'::uuid)
|
|
);
|
|
|
|
-- Lookup index on location_hash for application reads (kept lightweight here;
|
|
-- the larger covering index `tiles_leaflet_path` is owned by AZ-505).
|
|
CREATE INDEX IF NOT EXISTS idx_tiles_location_hash ON tiles (location_hash);
|
|
|
|
COMMIT;
|