# SQL Writes Through Raft (C3b) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** When Raft is enabled, SQL writes commit through the Raft log before the client sees success; followers reject writes naming the leader and apply committed entries via the existing applyCommand loop. **Architecture:** Intercept at the server-level `executeQuery` (core/server.nim:206-227) — same hook point and same data source (`res.keyValuePairs`) the legacy ReplicationManager uses. Leader: execute locally, append one raft entry per KV pair (`"put"` = `key\x00value`, `"delete"` = `key` — the exact format baradadb.nim's applyCommand already consumes), poll `node.commitIndex` until the last appended index commits (no raft.nim changes — state machine stays TLA-frozen). Follower: reject before execution. **Tech Stack:** Nim 2.2.10, ARC, unittest, real server processes for E2E. ## Global Constraints - Spec: `docs/superpowers/specs/2026-07-30-raft-sql-writes-design.md` (read first). - Test command per task: `nim c -d:ssl --threads:on --path:src -o:tests/test_all tests/test_all.nim && ./tests/test_all` — exit 0. Final task: full `nimble test` (673+ `[OK]`). - **raft.nim stays FROZEN** (state machine, handlers, message format, serialization — TLA-faithfulness tests pin them). All changes in server.nim / config.nim / exec/params.nim / executor.nim / baradadb.nim / tests. - Non-raft deployments (`server.raftNode == nil`): byte-identical behavior — verify the raft code path is fully gated. - No new dependencies. Commits per task after green; source files only; no push (controller merges+pushes). - Verified facts to use: - Server hook: `core/server.nim:206-227` — parse at :213, `executor.executeQuery` at :218, legacy replication ship at :219-227 (`replication.writeLsn(data)` with `key\0value`). `executeQuery(db, ctx, query, params, replication)` is called from handleClient (:567 plain, :587 prepared). - Statement kinds: `nkInsert/nkUpdate/nkDelete/nkMerge` write directly; `nkCommitTxn` emits kvPairs at COMMIT (executor.nim:968-979); BEGIN/ROLLBACK are connection-local. - appendLog (raft.nim:355): `appendLog*(node, command, data): LogEntry` — returns empty entry (index 0) when not leader; replication ships on the next heartbeat (50ms); commit advances via handleAppendReply → applyCommitted (raft.nim:195-215, 407-421). - applyCommand format (baradadb.nim:336-343): `cmd == "put"` → data = `key \x00 value`; `cmd == "delete"` → data = `key`. - Delete kvPair convention: non-txn DELETE emits `(fullKey, @[])` (exec/dml.nim:241). **OPEN ITEM for Task 2: txn COMMIT emits `(key, version.value)` even when `version.isDelete` (executor.nim:971-976) — check whether version.value is empty for deletes (read core/mvcc.nim writeSet/VersionedValue); if not, change the COMMIT loop to emit `(key, @[])` for deletes so the raft "empty value = delete" rule holds.** - Config env parsing: core/config.nim:177-206 (BARADB_RAFT_* section). --- ### Task 1: isWrite helper + Server.raftNode + follower rejection **Files:** - Modify: `src/barabadb/query/exec/params.nim` (isWrite helper next to isDDL) - Modify: `src/barabadb/core/server.nim` (raftNode field + rejection in executeQuery) - Modify: `src/baradadb.nim` (assign server.raftNode when raft enabled) - Test: `tests/bugfix_test.nim` **Interfaces:** - Produces: - `proc isWrite*(stmt: Node): bool` in exec/params.nim — true for `nkInsert, nkUpdate, nkDelete, nkMerge, nkCommitTxn` (check exact enum names in query/ast.nim; nkCommitTxn included because COMMIT emits kvPairs). - `Server.raftNode*: RaftNode` (nil default) — server.nim imports core/raft (verify no cycle: raft.nim must not import server.nim). - Rejection in server-level executeQuery (core/server.nim:206+): right after parse and the empty-stmts check, BEFORE `executor.executeQuery`: ```nim if server.raftNode != nil and isWrite(astNode.stmts[0]): let node = server.raftNode if node.state != rsLeader: let who = if node.leaderId.len > 0: node.leaderId else: "none elected" return (false, QueryResult(), "not leader; leader is '" & who & "'") ``` (Adjust to the proc's actual error-return convention and to how it accesses the Server — if executeQuery is not a method, thread the raft node the same way `replication` is threaded; check the handleClient call sites at :567/:587 first and pick the smaller change.) - [ ] **Step 1: Failing tests** New suite in tests/bugfix_test.nim: ```nim suite "Raft write classification": test "isWrite classifies DML and COMMIT": check isWrite(parse("INSERT INTO t (id) VALUES (1)").stmts[0]) check isWrite(parse("UPDATE t SET id = 2").stmts[0]) check isWrite(parse("DELETE FROM t WHERE id = 1").stmts[0]) check isWrite(parse("COMMIT").stmts[0]) check not isWrite(parse("SELECT * FROM t").stmts[0]) check not isWrite(parse("CREATE TABLE t (id INT)").stmts[0]) check not isWrite(parse("BEGIN").stmts[0]) check not isWrite(parse("ROLLBACK").stmts[0]) ``` (Adapt to bugfix_test.nim's imports — it has parser already.) - [ ] **Step 2: Run, watch it fail** Expected: compile error — `isWrite` undeclared. - [ ] **Step 3: Implement** The helper, the Server field, the rejection, the baradadb.nim assignment (`tcpServer.raftNode = raftNode` — check the actual server variable name and that raftNet/raftNode are in scope; they were hoisted in the C3a fix wave). - [ ] **Step 4: Run, watch it pass** bugfix_test green; test_all green (461+ [OK]). - [ ] **Step 5: Commit** ```bash git add src/barabadb/query/exec/params.nim src/barabadb/core/server.nim src/baradadb.nim tests/bugfix_test.nim git commit -m "feat(raft): classify writes, reject them on follower nodes" ``` --- ### Task 2: Leader write path — append + wait-for-commit **Files:** - Modify: `src/barabadb/core/server.nim` (raft append + commit wait, replacing the legacy ship when raft active) - Modify: `src/barabadb/core/config.nim` (raftWriteTimeoutMs + env) - Modify: possibly `src/barabadb/query/executor.nim` (COMMIT delete kvPair convention — see OPEN ITEM) - Test: `tests/test_all.nim` (in-process leader path) **Interfaces:** - Consumes: Task 1's isWrite + server.raftNode; appendLog; applyCommand format. - Produces: - In server executeQuery after `res.success` (replacing the legacy replication ship when raftNode != nil): ```nim if server.raftNode != nil and res.keyValuePairs.len > 0: let node = server.raftNode var lastIdx = 0'u64 for (key, value) in res.keyValuePairs: let entry = if value.len > 0: node.appendLog("put", cast[seq[byte]](key & "\x00" & cast[string](value))) else: node.appendLog("delete", cast[seq[byte]](key)) if entry.index == 0: return (false, QueryResult(), "lost leadership during raft append") lastIdx = entry.index # wait for commit let deadline = getMonoTime() + initDuration(milliseconds = config.raftWriteTimeoutMs) while node.commitIndex < lastIdx and getMonoTime() < deadline: await sleepAsync(10) # or the sync equivalent — check whether executeQuery is async; if it is NOT async, use os.sleep in a bounded loop if node.commitIndex < lastIdx: return (false, QueryResult(), "raft commit timeout") else if replication != nil and res.keyValuePairs.len > 0: ``` CRITICAL: check whether server-level executeQuery (server.nim:206) is a sync proc under withStorageGate — if sync, the wait must not block the async event loop; use short os.sleep polling and keep the timeout small, or document. Match the codebase's reality, not this sketch. - `raftWriteTimeoutMs*: int` on BaraConfig (default 5000) + `BARADB_RAFT_WRITE_TIMEOUT_MS` env parsing in the raft section of loadConfigFromEnv. - COMMIT delete convention resolved (OPEN ITEM above): version.value empty for deletes, or executor.nim COMMIT loop fixed to emit `(key, @[])` for isDelete entries. - [ ] **Step 1: Failing test** In tests/test_all.nim (raft suites): ```nim test "leader append+commit wait round-trips through applyCommand": # 3 in-process nodes over TCP (reuse the "timerLoop elects a leader" setup from C3a Task 2) # wait for a leader; on the leader: appendLog("put", "users.1\x00alice") # poll leader.commitIndex until >= entry.index (deadline 3s) # assert a follower's applyCommand got invoked with ("put", data) # (wire applyCommand on each node to record calls — check how baradadb.nim wires it) test "txn COMMIT delete kvPairs are empty-valued": # embedded ctx: BEGIN; INSERT; DELETE same row; COMMIT # assert res.keyValuePairs for the deleted key has value.len == 0 ``` - [ ] **Step 2: Run, watch them fail** Expected: first — no server-side helper exists yet (compile error or assertion); second — may already pass or fail depending on the OPEN ITEM finding; record which. - [ ] **Step 3: Implement** Per Produces. Resolve the sync/async question by reading server.nim:206-227 and its callers first; keep the wait bounded and simple. - [ ] **Step 4: Run, watch them pass** test_all green. - [ ] **Step 5: Commit** ```bash git add src/barabadb/core/server.nim src/barabadb/core/config.nim src/barabadb/query/executor.nim tests/test_all.nim git commit -m "feat(raft): leader appends writes to raft log and waits for commit" ``` --- ### Task 3: E2E — replicated writes across a real 3-node cluster **Files:** - Create: `tests/raft_writes_e2e_test.nim` (or extend tests/raft_e2e_test.nim — pick the cleaner; new file preferred to keep runtimes isolated) - Modify: `baradadb.nimble` (wire into test task) **Interfaces:** - Consumes: Tasks 1-2; the E2E process harness from tests/raft_e2e_test.nim (port base, env setup, leader detection via the "became leader" log line, teardown); the Nim client via `adaptors/nim/baradb_sqlite` (pattern from tests/nimforum_smoke_test.nim: `open("127.0.0.1:" & $port, "", "", "default")`, `db.exec(sql"...")`, `db.getAllRows`). - Produces: suite "Raft replicated writes E2E": 1. Boot 3 nodes (raft enabled), wait for leader (log line). 2. On the LEADER's client port: CREATE TABLE + INSERT a row (expect success). 3. On a FOLLOWER's client port: poll `SELECT` until the row appears (deadline 5s; follower applies committed entries via applyCommand to its default DB). 4. On a FOLLOWER: `INSERT` → expect an error containing "not leader". 5. Kill the leader; wait for new leader; INSERT on the new leader succeeds; the row becomes visible on the remaining follower. 6. Teardown all processes; dump captured output on any failure. - [ ] **Step 1: Write the test** Follow raft_e2e_test.nim conventions (O_NONBLOCK output drains, deadlines, skip-with-reason if build/baradadb missing, different port base from both nimforum_smoke_test and raft_e2e_test). Note: applyCommand applies to the DEFAULT database — use database "default" in the client. - [ ] **Step 2: Run standalone (twice)** `nim c -d:ssl --threads:on --path:src -o:tests/raft_writes_e2e_test tests/raft_writes_e2e_test.nim && ./tests/raft_writes_e2e_test` Expected: PASS twice consecutively. If the follower never sees the row, debug order: raft commit wait (Task 2) → applyCommand wiring → default-DB targeting. Dump output on failure. - [ ] **Step 3: Wire into nimble test + full suite** Add to baradadb.nimble test list after raft_e2e_test; add the binary name to .gitignore next to tests/raft_e2e_test. Run `nimble test` — exit 0, 673+ `[OK]`. - [ ] **Step 4: Commit** ```bash git add tests/raft_writes_e2e_test.nim baradadb.nimble .gitignore git commit -m "test(raft): E2E replicated writes, follower rejection, failover writes" ``` --- ### Task 4: Docs + close-out **Files:** - Modify: `docs/superpowers/specs/2026-07-30-raft-sql-writes-design.md` (status → done) - Modify: README.md only if it claims raft/replication behavior that changed (check `grep -n -i 'raft\|replicat' README.md | head -20` — update only now-false lines, minimally) - [ ] **Step 1: Update docs** - [ ] **Step 2: Commit** ```bash git add docs/ README.md git commit -m "docs: SQL writes through raft (C3b) done" ``` --- ## Self-Review Notes - Spec coverage: write classification → T1; follower rejection → T1; leader append+wait → T2; config → T2; E2E → T3; docs → T4. Non-goals honored (no forwarding, no DDL replication, raft.nim frozen). - The COMMIT delete-convention open item is the main correctness risk (a deleted key resurrected on followers) — T2's second test pins it before it can ship. - Double application on the leader is idempotent by KV semantics; T3's follower-visibility assertions prove the real path end-to-end.