[AZ-357] Refactor C06: drop tile Version concept; cumulative review batches 7-9
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful

AZ-357 — eliminate year-based tile cache expiry (LF-1):
- Migration 012: drop 5-col unique index, dedupe by (lat,lon,zoom,
  size) keeping max(updated_at), add new 4-col unique index, make
  version column nullable + drop default. Column itself preserved
  per coderule (column drops require explicit confirmation; tracked
  in AZ-373 / C20).
- TileEntity.Version, TileMetadata.Version, DownloadTileResponse.
  Version: int -> int? (HTTP shape preserved; field still in JSON).
- TileService.DownloadAndStoreTilesAsync: drop currentVersion year
  computation and the .Where(t => t.Version == currentVersion)
  cache filter. BuildTileEntity: drop year arg; write Version=null.
- TileRepository: ON CONFLICT now 4-col; lookup queries
  ORDER BY updated_at DESC instead of version DESC.
- Tests: replace inverted BT02b with positive AZ357_AC1
  (prior-year cached tile is reused). Add BuildTileEntity_
  DoesNotPopulateVersion_AZ357 to enforce the no-write contract.
- 69 unit + 5 smoke + 3 stub-contract integration tests pass.

Cumulative code review (batches 7-9, 7 tasks): VERDICT=PASS.
Report at _docs/03_implementation/reviews/batch_09_review.md.
Zero Critical/High/Medium/Low findings. Architecture baseline
remains clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:20:47 +03:00
parent 5a28f67d33
commit 581dff206e
12 changed files with 306 additions and 41 deletions
@@ -0,0 +1,31 @@
-- AZ-357 / C06: drop year-based versioning from the tile uniqueness key.
-- The 'version' column itself is preserved (intentionally; column drops require
-- explicit confirmation per coderule.mdc). This migration:
-- 1. Drops the 5-column unique index that includes version.
-- 2. Dedupes pre-existing duplicates by the new 4-column key, keeping the row
-- with the highest updated_at (tie-break: highest id).
-- 3. Recreates the unique index without version.
-- 4. Makes the version column nullable and drops its default so new rows can
-- be inserted without writing a meaningless year value.
DROP INDEX IF EXISTS idx_tiles_unique_location;
DELETE FROM tiles
WHERE id IN (
SELECT id
FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY latitude, longitude, tile_zoom, tile_size_meters
ORDER BY updated_at DESC, id DESC
) AS rn
FROM tiles
) ranked
WHERE rn > 1
);
CREATE UNIQUE INDEX idx_tiles_unique_location
ON tiles(latitude, longitude, tile_zoom, tile_size_meters);
ALTER TABLE tiles ALTER COLUMN version DROP NOT NULL;
ALTER TABLE tiles ALTER COLUMN version DROP DEFAULT;
@@ -12,7 +12,7 @@ public class TileEntity
public int TileSizePixels { get; set; }
public string ImageType { get; set; } = string.Empty;
public string? MapsVersion { get; set; }
public int Version { get; set; }
public int? Version { get; set; }
public string FilePath { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
@@ -42,7 +42,7 @@ public class TileRepository : ITileRepository
file_path as FilePath, created_at as CreatedAt, updated_at as UpdatedAt
FROM tiles
WHERE tile_zoom = @TileZoom AND tile_x = @TileX AND tile_y = @TileY
ORDER BY version DESC
ORDER BY updated_at DESC
LIMIT 1";
return await connection.QuerySingleOrDefaultAsync<TileEntity>(sql, new { TileZoom = tileZoom, TileX = tileX, TileY = tileY });
@@ -100,7 +100,7 @@ public class TileRepository : ITileRepository
WHERE latitude BETWEEN @MinLat AND @MaxLat
AND longitude BETWEEN @MinLon AND @MaxLon
AND tile_zoom = @TileZoom
ORDER BY version DESC, latitude DESC, longitude ASC";
ORDER BY latitude DESC, longitude ASC, updated_at DESC";
return await connection.QueryAsync<TileEntity>(sql, new
{
@@ -122,7 +122,7 @@ public class TileRepository : ITileRepository
VALUES (@Id, @TileZoom, @TileX, @TileY, @Latitude, @Longitude, @TileSizeMeters,
@TileSizePixels, @ImageType, @MapsVersion, @Version, @FilePath,
@CreatedAt, @UpdatedAt)
ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters, version)
ON CONFLICT (latitude, longitude, tile_zoom, tile_size_meters)
DO UPDATE SET
file_path = EXCLUDED.file_path,
tile_x = EXCLUDED.tile_x,