-- 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;