[indexer] Table-prefix-aware indexer (drain, create, populate)
## Problem
When the indexer detects a schema version mismatch, it must drain current work, create new-prefix tables, and index into them — all while existing web server pods continue serving from old tables.
## Proposal
### Mismatch detection
On startup, compare embedded `SCHEMA_VERSION` with the active version in `gkg_schema_version`. If they don't match, enter migration mode.
### Migration flow
1. **Acquire lock**: NATS KV `indexing_locks` bucket (short TTL with refresh). If another pod holds the lock, wait — it's handling the migration.
2. **Drain**: Stop consuming from NATS streams. Wait for all in-flight indexing jobs to complete. This ensures no partial writes to old-prefix tables during the transition.
3. **Create new-prefix tables**: Parse table names from embedded `config/graph.sql` (reuse existing `include_str!` pattern). For each `CREATE TABLE IF NOT EXISTS <table>`, emit `CREATE TABLE IF NOT EXISTS <prefix><table>` with the new version's prefix.
4. **Mark migrating**: Insert new version with status `migrating` in `gkg_schema_version`.
5. **Resume with new prefix**: Dispatcher dispatches backfill work for all enabled namespaces and code scopes. Indexer writes to new-prefix tables.
### Write path changes
All existing indexer write paths must use `prefixed_table_name()` for table references. This affects:
- SDLC handler (namespace indexing)
- Code indexing handler
- Checkpoint writes
- Namespace deletion handler
- Edge writes
The prefix is derived from the current `SCHEMA_VERSION` constant — during normal operation it matches the active version; during migration it targets the new version.
### Table name extraction
Parse table names from `config/graph.sql` at compile time rather than hardcoding:
```rust
fn parse_gkg_table_names(schema_sql: &str) -> Vec<String> {
// Parse CREATE TABLE IF NOT EXISTS <table_name> statements
// Returns: ["checkpoint", "namespace_deletion_schedule", "gl_user", ...]
}
```
This ensures the table list stays in sync with `graph.sql` automatically.
## Acceptance criteria
- [ ] Schema version mismatch triggers drain → create → populate flow
- [ ] NATS KV lock prevents concurrent migration by multiple pods
- [ ] New-prefix tables created from `config/graph.sql` with prefix applied
- [ ] All indexer write paths use prefixed table names
- [ ] Existing non-migration indexing continues to work with prefix
- [ ] `gkg_schema_version` updated to `migrating` status
- [ ] Metric: `gkg_schema_migration_total` (labels: `phase=drain|create|populate`, `result=success|failure`)
- [ ] Integration test: version mismatch triggers full migration flow
## Existing implementation to build on
- **!824** (`feat(indexer): add V0 schema version tracking and mismatch detection`) — periodic mismatch detection, enabled namespace counting, schema version reads. The V0 flow triggers a reset when zero namespaces are enabled; V0.5 replaces that with the drain → create → populate flow.
- **!809** (`feat(migration): add distributed lock and reconciler`) — NATS KV lock with TTL and CAS refresh, reconciler background task. The lock mechanism can be reused directly.
## Dependencies
- Issue 2: Schema version tracking with table prefix support
## Blocks
- Issue 5: Migration completion and cleanup
- Issue 6: E2E staging validation
issue