fix(raft): distinguish put-with-empty-value from delete in write path
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user