[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,77 @@
# Refactor: drop tile Version concept; latest row wins; new migration
**Task**: AZ-357_refactor_drop_tile_version
**Name**: Eliminate year-based tile versioning; cache by (lat, lon, zoom, tile_size)
**Description**: Remove the `Version` filter from tile-cache logic, change repository upsert semantics to (lat, lon, zoom, tile_size), and ship a migration that drops the 5-column unique constraint, replaces it with a 4-column one, and dedupes pre-existing duplicates.
**Complexity**: 5 points
**Dependencies**: None (C20 follows from this change)
**Component**: Services.TileDownloader + DataAccess
**Tracker**: AZ-357
**Epic**: AZ-350
## Problem
`SatelliteProvider.Services.TileDownloader/TileService.cs` uses `var currentVersion = DateTime.UtcNow.Year` and filters cached tiles via `existingTiles.Where(t => t.Version == currentVersion)`. On every Jan 1 UTC the year flips and the cache effectively expires (LF-1 in `discovery/logical_flow_analysis.md`). The `version` concept is unused as a real cache lever.
## Outcome
- Tile cache survives year boundaries (cached tiles from prior years remain valid).
- Repository lookups return the most recently updated row for each `(lat, lon, zoom, tile_size_meters)` cell.
- New rows are upserted on conflict by the 4-column key.
- DB unique constraint matches the new key; pre-existing duplicates are deduped (keeping highest `updated_at`).
- The `version` column itself is preserved (per `coderule.mdc` — no rename/drop without explicit confirmation).
- 37 unit + 5 smoke tests stay green; `migration_test_step1.md` (or equivalent) covers the migration.
## Scope
### Included
- Delete `t.Version == currentVersion` filter in `TileService.DownloadAndStoreTilesAsync`.
- Stop writing `currentVersion` into `TileEntity.Version` in `BuildTileEntity`.
- Update `TileRepository.GetTilesByRegionAsync` and `GetByTileCoordinatesAsync` to deduplicate on the 4-column key, returning the latest row per cell.
- Change `TileRepository.InsertAsync`'s `ON CONFLICT` clause to the 4-column key.
- Add a new migration SQL file (next number) that drops the 5-column unique constraint, dedupes pre-existing rows, then adds a new 4-column unique constraint.
- Add a unit/integration test that fakes `UtcNow` across a year boundary and verifies cache hit.
### Excluded
- Dropping the `version` column from `tiles` (deferred; per `coderule.mdc` no column drops without explicit confirmation).
- Touching `MapsVersion` (separate task: AZ-373 / C20).
## Acceptance Criteria
**AC-1: Cache survives year boundary**
Given a row in `tiles` with `version = 2025`
When the system queries the same `(lat, lon, zoom, tile_size_meters)` cell with the clock advanced into 2026
Then the cached row is returned (not re-downloaded).
**AC-2: Migration runs cleanly on populated tile data**
Given a `tiles` table containing duplicates by the new 4-column key (across different `version` values)
When the new migration runs
Then duplicates are collapsed to the row with the highest `updated_at`, and the new 4-column unique constraint exists.
**AC-3: Upsert behaves on the new key**
Given two `InsertAsync` calls with identical `(lat, lon, zoom, tile_size_meters)` and different `version` values
When both run
Then the table contains exactly one row for that cell (the second call updated the first).
**AC-4: Tests stay green**
Given the post-refactor build
When `scripts/run-tests.sh --smoke` runs
Then all 37 unit + 5 smoke scenarios pass.
## Constraints
- DB column `version` is preserved (left nullable; new code does not write to it).
- HTTP shape of `DownloadTileResponse` preserved (`Version` field still present in the JSON).
- No rename of any column.
## Risks & Mitigation
**Risk 1: production tile table contains duplicates that resolve ambiguously**
- *Risk*: if multiple rows share the new 4-column key with the same `updated_at`, the dedupe could pick the wrong row.
- *Mitigation*: tie-break on `id` (largest wins) within the dedupe SQL.
**Risk 2: rollback is hard once the migration runs**
- *Risk*: dropped duplicates are gone.
- *Mitigation*: migration SQL must be reviewable and tested against a populated copy before prod rollout. Capture pre-migration row counts in the migration log.
Full change entry: `_docs/04_refactoring/03-code-quality-refactoring/list-of-changes.md` (C06).