step 02 — SQL surface and MVCC
Goal
Add exec_update, exec_delete, select_by_tag, and the secondary
index. By the end of this step:
exec_updatemust be a no-op (and not bumpnext_txid) if the row is missing or tombstoned. If present, it keeps the originalcreated_atand only mutatesvandtag.exec_deletemust be a no-op if the row is missing or tombstoned. If present, it setsdeleted_at = next_txidand removes the row from the secondary index. The row stays in the primary.- The secondary index
tag -> sorted Vec<i64>is maintained on every mutating op. Only live rows are present. select_by_tag(tag)returns the secondary list, or empty.
Tasks
- Wire
exec_updateandexec_deletewith the no-op-on-missing rule. Test it by calling each on a key that does not exist and assertingnext_txiddid not move. - Implement secondary insertion as sorted insert (binary search +
shift, or
BTreeMap::entry().or_default()+ sorted insert). - Implement secondary removal as sorted lookup + erase. If the list becomes empty, drop the tag entirely (otherwise the snapshot will carry empty entries and diverge from the spec).
- Add a test that inserts three rows with the same tag in scrambled
key order, then asserts
select_by_tagreturns them in ascending order. - Add a test for the resurrection path: insert, delete, insert again
on the same key. The new row must have a fresh
created_atanddeleted_at == 0.
Pitfalls
- The most common bug is bumping
next_txidinexec_updateeven on a no-op. The unit tests in one language will pass; the cross-language hash will diverge after the first missing-key update. - Forgetting to drop an empty tag from
secondaryafter the last delete will add a zero-length entry to the snapshot dump and break cross-language byte equality. - In C++,
std::map::operator[]default-constructs missing entries silently — usefindfor reads and[]only when you intend the insert.