feat(raft): replicate schema DDL through the raft log
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
- isRaftDdl + leader-only gate for CREATE/DROP/ALTER (not DATABASE) - appendDdlToRaft ships original SQL; applyCommand re-executes via applyReplicatedDdl (idempotent on leader double-apply) - Mixed DDL+DML batches use the DDL path so order is preserved - Fix secondary-index point lookup to use entry.lsmKey (not filter col) - E2E: CREATE only on leader, schema + index SELECT on follower
This commit is contained in:
@@ -408,3 +408,11 @@ suite "Raft write classification":
|
||||
for s in ast.stmts:
|
||||
if isWrite(s): anyWrite = true
|
||||
check anyWrite
|
||||
|
||||
test "isRaftDdl covers schema but not CREATE DATABASE":
|
||||
check isRaftDdl(parse("CREATE TABLE t (id INT)").stmts[0])
|
||||
check isRaftDdl(parse("CREATE INDEX i ON t (id)").stmts[0])
|
||||
check isRaftDdl(parse("DROP TABLE t").stmts[0])
|
||||
check not isRaftDdl(parse("CREATE DATABASE other").stmts[0])
|
||||
check not isRaftDdl(parse("INSERT INTO t (id) VALUES (1)").stmts[0])
|
||||
check not isRaftDdl(parse("SELECT 1").stmts[0])
|
||||
|
||||
@@ -211,21 +211,62 @@ proc runWritesScenario() =
|
||||
return
|
||||
echo "leader elected: ", nodes[leaderIdx].id, " (term ", leaderTerm, ")"
|
||||
|
||||
# Schema: CREATE TABLE / INDEX are not raft writes (no kvPairs), and
|
||||
# _schema keys are not replicated — create them locally on every node.
|
||||
for i in 0 ..< nodes.len:
|
||||
let db = openClient(nodes[i].clientPort)
|
||||
let followerIdx = (if leaderIdx == 0: 1 else: 0)
|
||||
|
||||
# Schema: CREATE TABLE / INDEX go through the raft "ddl" log (C3c).
|
||||
# Create only on the leader; followers must learn schema via apply.
|
||||
block:
|
||||
let db = openClient(nodes[leaderIdx].clientPort)
|
||||
try:
|
||||
db.exec(sql"CREATE TABLE rw_test (id INT PRIMARY KEY, name STRING)")
|
||||
db.exec(sql"CREATE INDEX idx_rw_name ON rw_test (name)")
|
||||
except CatchableError as e:
|
||||
echo "CREATE TABLE/INDEX failed on ", nodes[i].id, ": ", e.msg
|
||||
echo "leader CREATE TABLE/INDEX failed: ", e.msg
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
db.close()
|
||||
echo "leader schema committed via raft ddl"
|
||||
|
||||
let followerIdx = (if leaderIdx == 0: 1 else: 0)
|
||||
# Follower rejection of DDL (not just DML).
|
||||
block:
|
||||
let db = openClient(nodes[followerIdx].clientPort)
|
||||
var rejected = false
|
||||
try:
|
||||
db.exec(sql"CREATE TABLE should_fail (id INT)")
|
||||
except CatchableError as e:
|
||||
rejected = "not leader" in e.msg
|
||||
if not rejected:
|
||||
echo "follower CREATE failed without 'not leader': ", e.msg
|
||||
db.close()
|
||||
if not rejected:
|
||||
echo "follower CREATE was not rejected with 'not leader'"
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
echo "follower CREATE rejected with 'not leader'"
|
||||
|
||||
# Wait until the follower has applied CREATE TABLE (SELECT no longer
|
||||
# errors with unknown table). Deadline 5s.
|
||||
block:
|
||||
let db = openClient(nodes[followerIdx].clientPort)
|
||||
defer: db.close()
|
||||
let start = getTime()
|
||||
var ready = false
|
||||
while getTime() - start < initDuration(seconds = 5):
|
||||
try:
|
||||
discard db.getAllRows(sql"SELECT * FROM rw_test")
|
||||
ready = true
|
||||
break
|
||||
except CatchableError:
|
||||
sleep(100)
|
||||
if not ready:
|
||||
echo "follower ", nodes[followerIdx].id,
|
||||
" never applied CREATE TABLE within 5s"
|
||||
dumpAll(nodes)
|
||||
fail()
|
||||
return
|
||||
echo "schema replicated to follower ", nodes[followerIdx].id
|
||||
|
||||
# Leader write: INSERT goes through the raft log and waits for majority
|
||||
# commit before responding (Task 2) — expect success.
|
||||
|
||||
@@ -2706,6 +2706,24 @@ suite "Raft SQL Write Path":
|
||||
check res.keyValuePairs.len == 1
|
||||
check res.keyValuePairs[0][1].len == 0
|
||||
|
||||
test "appendDdlToRaft fails when node is not leader":
|
||||
var n = newRaftNode("n1", @["n2"], raftPort = 29113)
|
||||
let (ok, err) = waitFor appendDdlToRaft(n,
|
||||
"CREATE TABLE t (id INT)", timeoutMs = 200)
|
||||
check not ok
|
||||
check "lost leadership" in err
|
||||
|
||||
test "applyReplicatedDdl creates table on empty context":
|
||||
var testDir = getTempDir() / "baradb_raft_ddl_" & $getCurrentProcessId() & "_" & $getMonoTime().ticks
|
||||
createDir(testDir)
|
||||
var db = newLSMTree(testDir)
|
||||
var ctx = qexec.newExecutionContext(db)
|
||||
applyReplicatedDdl(ctx, "CREATE TABLE ddl_t (id INT PRIMARY KEY, name STRING)")
|
||||
check "ddl_t" in ctx.tables
|
||||
# Idempotent re-apply (leader double-apply)
|
||||
applyReplicatedDdl(ctx, "CREATE TABLE ddl_t (id INT PRIMARY KEY, name STRING)")
|
||||
check "ddl_t" in ctx.tables
|
||||
|
||||
test "appendWriteToRaft fails when node is not leader":
|
||||
var n = newRaftNode("n1", @["n2"], raftPort = 29111)
|
||||
# Still a follower — appendLog returns index 0.
|
||||
|
||||
Reference in New Issue
Block a user