Commit 8dd9c46
feat(fts): 8c — cell-encoded persistence + on-demand v4→v5 bump (#80)
Phase 8 sub-phase 8c per docs/phase-8-plan.md. Persists FTS posting
lists as cell-encoded pages so save/reopen restores the index
bit-for-bit instead of re-tokenizing rows. Mirrors Phase 7d.3's
HNSW persistence shape across every layer.
User-visible: zero-friction. Existing v4 databases (no FTS) keep
writing v4 headers; the first save with an FTS index attached
promotes the file to v5. v5 readers handle both formats.
New:
- src/sql/pager/cell.rs:
- KIND_FTS_POSTING (0x06) — cell tag for FTS posting cells.
- src/sql/pager/fts_cell.rs (NEW):
- FtsPostingCell encode/decode. Either a posting cell
(term + [(rowid, term_freq)]) or, with empty term, the
sidecar carrying the per-doc length map. Sidecar preserves
every indexed doc — including zero-token rows — so total_docs
stays honest in BM25 post-reopen.
- src/sql/fts/posting_list.rs:
- serialize_doc_lengths / serialize_postings emit cell payloads
in deterministic order.
- from_persisted_postings reconstructs the index without
tokenization.
- src/sql/pager/header.rs:
- FORMAT_VERSION_V4 / FORMAT_VERSION_V5 constants
(FORMAT_VERSION_BASELINE = V4).
- DbHeader gains format_version field; encode_header writes
whatever the caller picked, decode_header accepts both
versions.
- src/sql/pager/mod.rs:
- stage_fts_btree / stage_fts_leaves stage one FTS index as a
TableLeaf-shaped B-Tree (sidecar first, then per-term cells).
- load_fts_postings walks leaves and decodes back into the
(doc_lengths, postings) shape.
- rebuild_fts_index gains the rootpage != 0 path (cell load),
keeping rootpage == 0 as the compatibility replay path for
v0.1.x→v0.2.0 upgraders.
- save_database stages each FTS index, writes its rootpage to
sqlrite_master, and conditionally bumps the header version
to v5 (preserving v4 when no FTS index is attached).
- parse_fts_create_index_sql helper, mirrors
parse_hnsw_create_index_sql.
Tests (engine 287 → 303 passing; +16 FTS-persistence-specific):
- src/sql/pager/fts_cell.rs (10): posting + sidecar round-trips,
empty postings, negative/large rowids, long term, 5000-entry
posting list, wrong kind tag, truncated buffer, invalid UTF-8
term, implausible count.
- src/sql/fts/posting_list.rs (1): serialize→from_persisted
round-trip including zero-token doc.
- src/sql/pager/mod.rs (5): persistence path is hit (rootpage != 0
in sqlrite_master), no-FTS save keeps v4, FTS save bumps to v5,
empty + zero-token round-trip, 500-doc multi-leaf round-trip.
This finishes the load-bearing 8a→8b→8c trio for the v0.2.0
release per docs/phase-8-plan.md.
Out of scope (later sub-phases):
- Hybrid retrieval worked example → 8d
- MCP bm25_search tool → 8e
- Docs sweep (fts.md, file-format.md, …) → 8f
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 1832d0e commit 8dd9c46
7 files changed
Lines changed: 927 additions & 23 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
79 | 143 | | |
80 | 144 | | |
81 | 145 | | |
| |||
413 | 477 | | |
414 | 478 | | |
415 | 479 | | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
416 | 507 | | |
417 | 508 | | |
418 | 509 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
73 | 73 | | |
74 | 74 | | |
75 | 75 | | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
76 | 100 | | |
77 | 101 | | |
78 | 102 | | |
| |||
0 commit comments