|
| 1 | +# TASK-029: Performance Hotspot Closure (schema/data version + persistent prepares) |
| 2 | + |
| 3 | +## Status |
| 4 | +- [ ] Planned |
| 5 | +- [ ] Assigned |
| 6 | +- [ ] In Progress |
| 7 | +- [ ] Blocked (reason: ...) |
| 8 | +- [x] Complete |
| 9 | + |
| 10 | +## Priority |
| 11 | +high |
| 12 | + |
| 13 | +## Assigned To |
| 14 | +subagent (general) |
| 15 | + |
| 16 | +## Parent Docs / Cross-links |
| 17 | +- Gap backlog: `research/zig-cr/92-gap-backlog.md` ("Remaining Gaps → Performance Optimizations") |
| 18 | +- Hotspot analysis: `research/zig-cr/11-performance-hotspots.md` |
| 19 | +- Likely code hotspots: `zig/src/stmt_cache.zig`, `zig/src/changes_vtab.zig`, `zig/src/ffi/api.zig` |
| 20 | + |
| 21 | +## Description |
| 22 | +Close the remaining post-MVP performance gaps that are explicitly tracked but not yet implemented: |
| 23 | + |
| 24 | +1) `PRAGMA schema_version` keyed invalidation caching (avoid unnecessary union rebuild / stmt recreation) |
| 25 | +2) `PRAGMA data_version` check amortization (avoid polling/pragma spam on hot loops) |
| 26 | +3) Prefer `sqlite3_prepare_v3(..., SQLITE_PREPARE_PERSISTENT)` for long-lived statements |
| 27 | + |
| 28 | +This task is intentionally narrow: improve hot-path behavior without changing query semantics. |
| 29 | + |
| 30 | +## Files to Modify |
| 31 | +- `zig/src/stmt_cache.zig` |
| 32 | +- `zig/src/changes_vtab.zig` |
| 33 | +- `zig/src/ffi/api.zig` (if prepare_v3 / flags wiring needed) |
| 34 | +- `research/zig-cr/92-gap-backlog.md` (check off items + add brief notes) |
| 35 | + |
| 36 | +## Acceptance Criteria |
| 37 | +- [x] `stmt_cache` exposes an explicit "schema version changed" signal that callers can use to invalidate cached derived artifacts. |
| 38 | +- [x] `changes_vtab` avoids rebuilding or repreparing union statements when schema_version unchanged. |
| 39 | +- [x] `PRAGMA data_version` checks are amortized (e.g. once per transaction / once per cursor scan loop) with no correctness regressions. |
| 40 | +- [x] If supported by the SQLite target, long-lived statements are prepared using `sqlite3_prepare_v3` with `SQLITE_PREPARE_PERSISTENT`. |
| 41 | +- [x] `make test-unit` and `make test-parity` pass (pre-existing rowid slab failures excluded). |
| 42 | + |
| 43 | +## Progress Log |
| 44 | +### 2025-12-14 |
| 45 | +- Task created during gap review; not yet started |
| 46 | +- Task started - implementing 3 performance optimizations |
| 47 | +- Added `prepare_v3` wrapper with `SQLITE_PREPARE_PERSISTENT` flag to `api.zig` |
| 48 | +- Added `schemaVersionChanged()` and `getSchemaVersion()` to `stmt_cache.zig` for invalidation signaling |
| 49 | +- Added `checkDataVersionAmortized()` and `resetDataVersionCheck()` to `stmt_cache.zig` for per-transaction amortization |
| 50 | +- Added `getOrPreparePersistent()` and `prepareOncePersistent()` for long-lived statements |
| 51 | +- Added schema-version keyed table name cache to `ChangesVTab` in `changes_vtab.zig` |
| 52 | +- Updated `changesFilter` to use schema-version keyed cache, avoiding re-query of sqlite_master when schema unchanged |
| 53 | +- Updated `discoverTablesCached` to use `prepareOncePersistent` for persistent statement preparation |
| 54 | +- All unit tests pass (64/64) |
| 55 | +- Parity tests pass except for pre-existing rowid slab failures (48/52) |
| 56 | + |
| 57 | +## Completion Notes |
| 58 | +### 2025-12-14 |
| 59 | + |
| 60 | +**Implemented:** |
| 61 | + |
| 62 | +1. **`PRAGMA schema_version` keyed invalidation caching** (`stmt_cache.zig`, `changes_vtab.zig`) |
| 63 | + - Added `schema_changed_flag` field to `StmtCache` that is set when schema version changes |
| 64 | + - Added `schemaVersionChanged()` method to check and consume the flag |
| 65 | + - Added `getSchemaVersion()` to get cached version without re-checking |
| 66 | + - Added schema-version keyed table name cache to `ChangesVTab` struct |
| 67 | + - `changesFilter` now reuses cached table names when schema_version is unchanged |
| 68 | + - New helper functions: `getCachedTableNames()`, `setCachedTableNames()`, `freeCachedTableNames()`, `copyTableNamesToCursor()`, `discoverTablesCachedWithInvalidation()` |
| 69 | + |
| 70 | +2. **`PRAGMA data_version` check amortization** (`stmt_cache.zig`) |
| 71 | + - Added `data_version_checked_this_txn` flag to `StmtCache` |
| 72 | + - Added `checkDataVersionAmortized()` that only performs actual PRAGMA query once per transaction |
| 73 | + - Added `resetDataVersionCheck()` for clearing the flag at transaction boundaries |
| 74 | + - Added `getDataVersion()` to get cached value |
| 75 | + |
| 76 | +3. **`sqlite3_prepare_v3` with `SQLITE_PREPARE_PERSISTENT`** (`api.zig`, `stmt_cache.zig`) |
| 77 | + - Added `prepare_v3()` wrapper to `api.zig` with fallback to `prepare_v2` if not available |
| 78 | + - Added `SQLITE_PREPARE_PERSISTENT`, `SQLITE_PREPARE_NORMALIZE`, `SQLITE_PREPARE_NO_VTAB` constants |
| 79 | + - Added `getOrPreparePersistent()` method to `StmtCache` |
| 80 | + - Added standalone `prepareOncePersistent()` helper function |
| 81 | + - Updated `checkSchemaVersion()`, `checkDataVersion()`, and `discoverTablesCached()` to use persistent prepares |
| 82 | + |
| 83 | +**Test Results:** |
| 84 | +- Unit tests: 64/64 PASS |
| 85 | +- Parity tests: 48/52 (4 pre-existing rowid slab failures, not related to this task) |
0 commit comments