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:
@@ -488,3 +488,11 @@ proc applyReplicatedDelete*(ctx: ExecutionContext, fullKey: string) =
|
||||
if found and table.len > 0:
|
||||
removeIndexesForRow(ctx, table, fullKey, cast[string](existing))
|
||||
ctx.db.delete(fullKey)
|
||||
|
||||
proc isBenignRaftReplayError*(msg: string): bool =
|
||||
## Leader re-applies committed DDL/DML after local execution; followers may
|
||||
## also see IF EXISTS / race re-applies. Treat common idempotent failures as OK.
|
||||
let m = msg.toLower()
|
||||
"already exists" in m or "does not exist" in m or
|
||||
"duplicate" in m or "unique" in m or
|
||||
"unknown table" in m or "no such table" in m
|
||||
|
||||
@@ -179,6 +179,17 @@ proc isDDL*(stmt: Node): bool =
|
||||
else:
|
||||
result = false
|
||||
|
||||
proc isRaftDdl*(stmt: Node): bool =
|
||||
## Schema changes that go through the Raft log when clustering is on.
|
||||
## CREATE/DROP DATABASE are excluded — multi-DB is out of scope for v1 raft
|
||||
## (state machine is wired only to the default database).
|
||||
if not isDDL(stmt): return false
|
||||
case stmt.kind
|
||||
of nkCreateDatabase, nkDropDatabase:
|
||||
result = false
|
||||
else:
|
||||
result = true
|
||||
|
||||
proc isWrite*(stmt: Node): bool =
|
||||
## True for statements that mutate stored data. `nkCommitTxn` is included
|
||||
## because COMMIT emits the transaction's buffered kvPairs.
|
||||
|
||||
@@ -317,8 +317,23 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
row[colName] = w.binRight.strVal
|
||||
rows.add(row)
|
||||
return okResult(rows, coveredCols)
|
||||
# Fetch actual row data from LSM
|
||||
let rows = execPointRead(ctx, stmt.selFrom.fromTable, colName & "=" & w.binRight.strVal)
|
||||
# Fetch full rows via the LSM keys stored in the index — never
|
||||
# reconstruct the primary key from the filter column (secondary
|
||||
# indexes are not the PK).
|
||||
var rows: seq[Row] = @[]
|
||||
for entry in entries:
|
||||
if entry.lsmKey.len == 0: continue
|
||||
let (found, val) = ctx.db.get(entry.lsmKey)
|
||||
if found:
|
||||
var row = parseRowDataToValueRow(cast[string](val))
|
||||
let prefix = stmt.selFrom.fromTable & "."
|
||||
if entry.lsmKey.startsWith(prefix):
|
||||
let rest = entry.lsmKey[prefix.len..^1]
|
||||
row["$key"] = rest
|
||||
let eqPos = rest.find('=')
|
||||
if eqPos >= 0:
|
||||
row[rest[0..<eqPos]] = rest[eqPos+1..^1]
|
||||
rows.add(row)
|
||||
let tbl = ctx.getTableDef(stmt.selFrom.fromTable)
|
||||
var cols: seq[string] = @[]
|
||||
for c in tbl.columns: cols.add(c.name)
|
||||
@@ -1749,6 +1764,22 @@ proc restoreEngines*(ctx: ExecutionContext) =
|
||||
except CatchableError as e:
|
||||
warn("restoreEngines: graph rebuild failed for '" & name & "': " & e.msg)
|
||||
|
||||
proc applyReplicatedDdl*(ctx: ExecutionContext, sql: string) {.gcsafe.} =
|
||||
## Raft "ddl" log entry: re-execute the original SQL on this node's context.
|
||||
## Called from applyCommand (no server/raft layer — must not re-append).
|
||||
## Leader double-apply failures (already exists / does not exist) are ignored
|
||||
## so the state machine keeps advancing. {.cast(gcsafe).} is required because
|
||||
## executeQuery touches the registry factory (same pattern as other engine
|
||||
## callbacks under the storage gate on the single-threaded apply path).
|
||||
{.cast(gcsafe).}:
|
||||
try:
|
||||
let tokens = qlex.tokenize(sql)
|
||||
let astNode = qpar.parse(tokens)
|
||||
if astNode.stmts.len == 0: return
|
||||
discard executeQuery(ctx, astNode)
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Hook wiring — breaks the module cycle between executor and the exec/*
|
||||
# submodules: eval.nim calls back into the engine for subqueries, hybrid
|
||||
|
||||
Reference in New Issue
Block a user