# 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).
