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
+6 -6
View File
@@ -45,7 +45,7 @@ proc violatesUniqueIndex*(ctx: ExecutionContext, table: string, fields: seq[stri
return ""
proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]],
kvPairs: var seq[(string, seq[byte])]): int =
kvPairs: var seq[tuple[key: string, value: seq[byte], deleted: bool]]): int =
if not hasPrivilege(ctx, table, "INSERT"):
return 0
let tblDef = if table in ctx.tables: ctx.tables[table] else: TableDef()
@@ -87,7 +87,7 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
discard ctx.txnManager.write(ctx.pendingTxn, fullKey, cast[seq[byte]](valStr))
else:
ctx.db.put(fullKey, cast[seq[byte]](valStr))
kvPairs.add((fullKey, cast[seq[byte]](valStr)))
kvPairs.add((fullKey, cast[seq[byte]](valStr), false))
for colName in ctx.btrees.keys.toSeq():
if colName.startsWith(table & "."):
@@ -221,7 +221,7 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
return count
proc execDelete*(ctx: ExecutionContext, table: string, key: string,
kvPairs: var seq[(string, seq[byte])]): int =
kvPairs: var seq[tuple[key: string, value: seq[byte], deleted: bool]]): int =
if not hasPrivilege(ctx, table, "DELETE"):
return 0
let fullKey = table & "." & key
@@ -238,7 +238,7 @@ proc execDelete*(ctx: ExecutionContext, table: string, key: string,
discard ctx.txnManager.delete(ctx.pendingTxn, fullKey)
else:
ctx.db.delete(fullKey)
kvPairs.add((fullKey, @[]))
kvPairs.add((fullKey, @[], true))
# Update BTree indexes
for colName in ctx.btrees.keys.toSeq():
if colName.startsWith(table & "."):
@@ -264,7 +264,7 @@ proc execDelete*(ctx: ExecutionContext, table: string, key: string,
return 0
proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Table[string, string],
kvPairs: var seq[(string, seq[byte])]): int =
kvPairs: var seq[tuple[key: string, value: seq[byte], deleted: bool]]): int =
if not hasPrivilege(ctx, table, "UPDATE"):
return 0
let fullKey = table & "." & key
@@ -313,7 +313,7 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
discard ctx.txnManager.write(ctx.pendingTxn, fullKey, cast[seq[byte]](newVal))
else:
ctx.db.put(fullKey, cast[seq[byte]](newVal))
kvPairs.add((fullKey, cast[seq[byte]](newVal)))
kvPairs.add((fullKey, cast[seq[byte]](newVal), false))
# Update FTS indexes: remove old doc, add new
for ftsKey, ftsIdx in ctx.ftsIndexes:
if ftsKey.startsWith(table & "."):
-1
View File
@@ -923,7 +923,6 @@ proc evalExprOld*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContex
ddl.add("\n")
# Sample data
var kvPairs: seq[(string, seq[byte])] = @[]
let rows = requireExecScanHook()(ctx, table)
let sampleLimit = min(5, rows.len)
if sampleLimit > 0:
+4 -4
View File
@@ -30,14 +30,14 @@ proc enforceFkOnDelete*(ctx: ExecutionContext, parentTable: string, parentCol: s
of "CASCADE":
for refRow in refs:
if "$key" in refRow:
var dummy: seq[(string, seq[byte])] = @[]
var dummy: seq[tuple[key: string, value: seq[byte], deleted: bool]] = @[]
discard execDelete(ctx, childTblName, valueToString(refRow["$key"]), dummy)
of "SET NULL":
for refRow in refs:
if "$key" in refRow:
var sets = initTable[string, string]()
sets[col.name] = "\\N"
var dummy: seq[(string, seq[byte])] = @[]
var dummy: seq[tuple[key: string, value: seq[byte], deleted: bool]] = @[]
discard execUpdateRow(ctx, childTblName, valueToString(refRow["$key"]), sets, dummy)
of "RESTRICT", "NO ACTION":
return (false, "FOREIGN KEY violation: row is referenced by " & childTblName & "." & col.name)
@@ -56,14 +56,14 @@ proc enforceFkOnUpdate*(ctx: ExecutionContext, parentTable: string, parentCol: s
if "$key" in refRow:
var sets = initTable[string, string]()
sets[col.name] = newVal
var dummy: seq[(string, seq[byte])] = @[]
var dummy: seq[tuple[key: string, value: seq[byte], deleted: bool]] = @[]
discard execUpdateRow(ctx, childTblName, valueToString(refRow["$key"]), sets, dummy)
of "SET NULL":
for refRow in refs:
if "$key" in refRow:
var sets = initTable[string, string]()
sets[col.name] = "\\N"
var dummy: seq[(string, seq[byte])] = @[]
var dummy: seq[tuple[key: string, value: seq[byte], deleted: bool]] = @[]
discard execUpdateRow(ctx, childTblName, valueToString(refRow["$key"]), sets, dummy)
of "RESTRICT", "NO ACTION":
return (false, "FOREIGN KEY violation: row is referenced by " & childTblName & "." & col.name)
+2 -2
View File
@@ -136,13 +136,13 @@ type
rows*: seq[Row]
affectedRows*: int
message*: string
keyValuePairs*: seq[(string, seq[byte])]
keyValuePairs*: seq[tuple[key: string, value: seq[byte], deleted: bool]]
proc `==`*(a, b: IndexEntry): bool =
a.lsmKey == b.lsmKey and a.rowValue == b.rowValue
proc okResult*(rows: seq[Row] = @[], cols: seq[string] = @[], affected: int = 0, msg: string = "",
kvPairs: seq[(string, seq[byte])] = @[]): ExecResult =
kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]] = @[]): ExecResult =
ExecResult(success: true, columns: cols, rows: rows, affectedRows: affected, message: msg,
keyValuePairs: kvPairs)
+11 -10
View File
@@ -574,7 +574,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
row[f] = mutableValues[0][i]
fireTriggers(ctx, stmt.insTarget, "before", "insert", row)
var kvPairs: seq[(string, seq[byte])]
var kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
let count = execInsert(ctx, stmt.insTarget, mutableFields, mutableValues, kvPairs)
# Fire AFTER INSERT triggers
@@ -626,7 +626,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
# Scan and apply
let rows = execScan(ctx, stmt.updTarget)
var count = 0
var kvPairs: seq[(string, seq[byte])]
var kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
for row in rows:
# Compute sets for this row (expressions may reference columns)
var sets = initTable[string, string]()
@@ -701,7 +701,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
# Delete all rows matching WHERE
let rows = execScan(ctx, stmt.delTarget)
var count = 0
var kvPairs: seq[(string, seq[byte])]
var kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
for row in rows:
if stmt.delWhere != nil and stmt.delWhere.whereExpr != nil:
let whereExpr = lowerExpr(stmt.delWhere.whereExpr)
@@ -744,7 +744,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
let targetRows = execScan(ctx, stmt.mergeTarget)
var count = 0
var kvPairs: seq[(string, seq[byte])]
var kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
for srcRow in sourceRows:
var matched = false
@@ -793,7 +793,7 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
for i, f in fields:
if i < values.len: row[f] = Value(kind: vkString, strVal: values[i])
fireTriggers(ctx, stmt.mergeTarget, "before", "insert", row)
var insKvPairs: seq[(string, seq[byte])]
var insKvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
count += execInsert(ctx, stmt.mergeTarget, fields, @[values], insKvPairs)
for kv in insKvPairs: kvPairs.add(kv)
fireTriggers(ctx, stmt.mergeTarget, "after", "insert", row)
@@ -982,16 +982,17 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
of nkCommitTxn:
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
var kvPairs: seq[(string, seq[byte])]
var kvPairs: seq[tuple[key: string, value: seq[byte], deleted: bool]]
for key, version in ctx.pendingTxn.writeSet:
if version.isDelete:
ctx.db.delete(key)
# Empty value is the raft/replication "delete" convention — never
# ship a non-empty body for isDelete or followers will resurrect.
kvPairs.add((key, @[]))
# Empty value + deleted=true is the raft/replication "delete"
# convention — never ship a non-empty body for isDelete or
# followers will resurrect.
kvPairs.add((key, @[], true))
else:
ctx.db.put(key, version.value)
kvPairs.add((key, version.value))
kvPairs.add((key, version.value, false))
discard ctx.txnManager.commit(ctx.pendingTxn)
ctx.pendingTxn = nil
return okResult(msg="Transaction committed", kvPairs=kvPairs)