fix(raft): distinguish put-with-empty-value from delete in write path

This commit is contained in:
2026-07-31 00:58:51 +03:00
parent 63cb05afe2
commit 431334b70a
9 changed files with 119 additions and 51 deletions
+73
View File
@@ -4,6 +4,7 @@ import std/os
import std/tables
import ../src/barabadb/query/[parser, executor, lexer, ast]
import ../src/barabadb/query/exec/params
import ../src/barabadb/query/exec/dml
import ../src/barabadb/core/types
import ../src/barabadb/core/config
import ../src/barabadb/storage/lsm
@@ -417,6 +418,78 @@ suite "Raft peer address parsing":
check msg.len > 0
check bad in msg
suite "Raft put/delete encoding — empty value is not a delete":
test "PK-only INSERT yields a put pair (deleted == false, empty value)":
var ctx = setupCtx()
defer: teardown(ctx)
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
let r = executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (1)"))
check r.success
check r.keyValuePairs.len == 1
check r.keyValuePairs[0].value.len == 0
check r.keyValuePairs[0].deleted == false
test "DELETE yields a delete pair (deleted == true)":
var ctx = setupCtx()
defer: teardown(ctx)
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
let r = executeQuery(ctx, parse("DELETE FROM users WHERE id = 1"))
check r.success
check r.keyValuePairs.len == 1
check r.keyValuePairs[0].deleted == true
check r.keyValuePairs[0].value.len == 0
test "UPDATE yields a put pair (deleted == false)":
var ctx = setupCtx()
defer: teardown(ctx)
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
let r = executeQuery(ctx, parse("UPDATE users SET name = 'bob' WHERE id = 1"))
check r.success
check r.keyValuePairs.len == 1
check r.keyValuePairs[0].deleted == false
check r.keyValuePairs[0].value.len > 0
test "txn COMMIT pairs carry deleted flag for buffered writes":
var ctx = setupCtx()
defer: teardown(ctx)
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
discard executeQuery(ctx, parse("INSERT INTO users (id, name) VALUES (1, 'alice')"))
discard executeQuery(ctx, parse("BEGIN"))
discard executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (7)"))
discard executeQuery(ctx, parse("DELETE FROM users WHERE id = 1"))
let r = executeQuery(ctx, parse("COMMIT"))
check r.success
check r.keyValuePairs.len == 2
var sawPut = false
var sawDelete = false
for pair in r.keyValuePairs:
if pair.deleted:
sawDelete = true
check pair.value.len == 0
else:
sawPut = true
check pair.key == "pkonly.id=7"
check pair.value.len == 0 # empty value must still be a put
check sawPut and sawDelete
test "apply of a put with empty value keeps the PK-only row":
var ctx = setupCtx()
defer: teardown(ctx)
discard executeQuery(ctx, parse("CREATE TABLE pkonly (id INTEGER PRIMARY KEY)"))
let r = executeQuery(ctx, parse("INSERT INTO pkonly (id) VALUES (3)"))
check r.success
check r.keyValuePairs.len == 1
let pair = r.keyValuePairs[0]
# Same decode as applyCommand in src/baradadb.nim for a "put" entry.
let encoded = pair.key & "\x00" & cast[string](pair.value)
let parts = encoded.split("\x00")
check parts.len >= 2
applyReplicatedPut(ctx, parts[0], cast[seq[byte]](parts[1]))
let sel = executeQuery(ctx, parse("SELECT * FROM pkonly WHERE id = 3"))
check sel.success
check sel.rows.len == 1
suite "Raft write classification":
test "isWrite classifies DML and COMMIT":
+3 -13
View File
@@ -7,16 +7,6 @@
## follower has caught up.
## Process-management conventions follow tests/raft_writes_e2e_test.nim;
## client access follows tests/nimforum_smoke_test.nim.
##
## NOTE: load_test deliberately has a non-PK column. A table whose only
## column is the PK stores an EMPTY LSM value per row (execInsert drops PK
## columns from the value), and the raft write path (appendWriteToRaft in
## src/barabadb/core/server.nim) encodes empty values as "delete" entries —
## so on commit every node applies a delete over the just-inserted row and
## it vanishes everywhere. That is a v1.2.0 product bug in the raft entry
## encoding (put/delete must not be inferred from value emptiness); until it
## is fixed in src/, this test exercises the two-column row shape that the
## current encoding handles correctly.
import std/unittest
import std/osproc
import std/os
@@ -176,7 +166,7 @@ proc writerLoop(args: WriterArgs) {.thread.} =
sleep(50)
continue
try:
db.exec(sql("INSERT INTO load_test (id, n) VALUES (" & $n & ", " & $n & ")"))
db.exec(sql("INSERT INTO load_test (id) VALUES (" & $n & ")"))
withLock args.lock[]:
args.acked[].add n
inc n
@@ -290,7 +280,7 @@ proc runFailoverLoadScenario() =
block:
let db = openClient(nodes[leaderIdx].clientPort)
try:
db.exec(sql"CREATE TABLE load_test (id INT PRIMARY KEY, n INT)")
db.exec(sql"CREATE TABLE load_test (id INT PRIMARY KEY)")
except CatchableError as e:
echo "leader CREATE TABLE failed: ", e.msg
dumpAll(nodes)
@@ -343,7 +333,7 @@ proc runFailoverLoadScenario() =
try:
let db = openClient(nodes[i].clientPort)
try:
db.exec(sql("INSERT INTO load_test (id, n) VALUES (" & $probeId & ", " & $probeId & ")"))
db.exec(sql("INSERT INTO load_test (id) VALUES (" & $probeId & ")"))
writerSurvivor = i
finally:
db.close()
+3 -3
View File
@@ -2670,7 +2670,7 @@ suite "Raft SQL Write Path":
# Server-side leader write path: append + wait for majority commit
let (ok, errMsg) = waitFor appendWriteToRaft(leader,
@[("users.1", cast[seq[byte]]("alice"))], timeoutMs = 3000)
@[("users.1", cast[seq[byte]]("alice"), false)], timeoutMs = 3000)
check ok
if not ok: echo "appendWriteToRaft failed: ", errMsg
@@ -2782,7 +2782,7 @@ suite "Raft SQL Write Path":
var n = newRaftNode("n1", @["n2"], raftPort = 29111)
# Still a follower — appendLog returns index 0.
let (ok, err) = waitFor appendWriteToRaft(n,
@[("k", cast[seq[byte]]("v"))], timeoutMs = 200)
@[("k", cast[seq[byte]]("v"), false)], timeoutMs = 200)
check not ok
check "lost leadership" in err
@@ -2791,7 +2791,7 @@ suite "Raft SQL Write Path":
var n = newRaftNode("n1", @["n2", "n3"], raftPort = 29112)
n.becomeLeader()
let (ok, err) = waitFor appendWriteToRaft(n,
@[("k", cast[seq[byte]]("v"))], timeoutMs = 300)
@[("k", cast[seq[byte]]("v"), false)], timeoutMs = 300)
check not ok
check "raft commit timeout" in err