feat(db): adopt zug for schema migrations (phase 3)
Replaces the open-coded schema.zig migration loop (51 ALTER/UPDATE
statements re-running every boot, consuming ~5min of turso for
full-table scans) with a zug.Migration list run via zug.sqlite.run.
## structure
- backend/src/db/migrations.zig — 10 zug.Migration entries
- 001_initial_schema: full current schema (every CREATE TABLE /
VIRTUAL TABLE / INDEX with all columns currently in production)
- 002_init_stats_started_at: stamp service_started_at via
strftime('%s','now') so it's part of the migration system, not
a runtime side-effect
- 003..010: data backfills (platform classification, base_path
denormalization, indexed_at backfills, greengale cleanup)
- backend/src/db/schema.zig — slimmed down to:
- bootstrapIfNeeded: if 'documents' exists but 'zug_migrations'
doesn't, seed every migration as already-applied (production is
already in this state, so re-running 5min of full-table scans
is wasteful)
- zug.sqlite.run with diagnostics piped through logfire on failure
- build.zig + build.zig.zon: zug as a build dep, pinned to commit
5fac77d so future zug pushes don't change the dep hash silently
## tests (52 pass)
- migration id uniqueness (catches dupes at build time, not zug
runtime)
- every migration has SQL (no callback path used)
- migration ids start with NNN_ for stable ordering
## not yet done
- live integration test against a turso scratch namespace — the
bootstrap path's behavior is testable but not yet covered
- moving initSchema off the startup gate in main.zig — once we
confirm zug.sqlite.run is fast on a bootstrapped DB this becomes
optional polish
Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>