refactor(exec): extract FK enforcement into exec/fk.nim

This commit is contained in:
2026-07-30 14:35:15 +03:00
parent 70b7ec7f08
commit 70c7297e33
2 changed files with 101 additions and 90 deletions
+100
View File
@@ -0,0 +1,100 @@
## Foreign-key enforcement — referential checks and cascade actions.
##
## Extracted from `executor.nim` (Task 10 of the executor split).
import std/strutils
import std/tables
import ../../storage/lsm
import types
import values
import scan
import dml
# ----------------------------------------------------------------------
# Foreign Key Enforcement
# ----------------------------------------------------------------------
proc findReferencingRows*(ctx: ExecutionContext, childTable: string, fkCol: string, fkValue: string): seq[Row] =
result = @[]
for row in execScan(ctx, childTable):
if fkCol in row and valueToString(row[fkCol]) == fkValue:
result.add(row)
proc enforceFkOnDelete*(ctx: ExecutionContext, parentTable: string, parentCol: string, parentVal: string): (bool, string) =
for childTblName, childTbl in ctx.tables:
for col in childTbl.columns:
if col.fkTable == parentTable and col.fkColumn == parentCol:
let action = if col.fkOnDelete.len > 0: col.fkOnDelete else: "RESTRICT"
let refs = findReferencingRows(ctx, childTblName, col.name, parentVal)
if refs.len > 0:
case action
of "CASCADE":
for refRow in refs:
if "$key" in refRow:
var dummy: seq[(string, seq[byte])] = @[]
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])] = @[]
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)
return (true, "")
proc enforceFkOnUpdate*(ctx: ExecutionContext, parentTable: string, parentCol: string, oldVal: string, newVal: string): (bool, string) =
for childTblName, childTbl in ctx.tables:
for col in childTbl.columns:
if col.fkTable == parentTable and col.fkColumn == parentCol:
let action = if col.fkOnUpdate.len > 0: col.fkOnUpdate else: "RESTRICT"
let refs = findReferencingRows(ctx, childTblName, col.name, oldVal)
if refs.len > 0:
case action
of "CASCADE":
for refRow in refs:
if "$key" in refRow:
var sets = initTable[string, string]()
sets[col.name] = newVal
var dummy: seq[(string, seq[byte])] = @[]
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])] = @[]
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)
return (true, "")
proc enforceFkOnChildUpdate*(ctx: ExecutionContext, childTable: string, fkCol: string, newVal: string): (bool, string) =
let tbl = ctx.getTableDef(childTable)
var parentTable = ""
var parentCol = ""
for col in tbl.columns:
if col.name == fkCol:
parentTable = col.fkTable
parentCol = col.fkColumn
break
if parentTable.len == 0 or parentCol.len == 0:
return (true, "")
if isNull(newVal):
return (true, "")
let fkKey = parentTable & "." & parentCol & "=" & newVal
let (fkExists, _) = ctx.db.get(fkKey)
if fkExists:
return (true, "")
var found = false
let prefix = parentTable & "."
for entry in ctx.db.scanMemTable():
if entry.deleted: continue
if entry.key.startsWith(prefix):
let rest = entry.key[prefix.len..^1]
if rest.startsWith(parentCol & "=") and rest[parentCol.len+1..^1] == newVal:
found = true
break
if not found:
return (false, "FOREIGN KEY violation: '" & newVal & "' not found in " & parentTable & "." & parentCol)
return (true, "")
+1 -90
View File
@@ -49,6 +49,7 @@ import exec/eval
import exec/lower import exec/lower
import exec/scan # internal — not re-exported import exec/scan # internal — not re-exported
import exec/dml import exec/dml
import exec/fk # internal — not re-exported
export types export types
export values export values
export schema export schema
@@ -68,96 +69,6 @@ proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row]
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult
# ----------------------------------------------------------------------
# Foreign Key Enforcement
# ----------------------------------------------------------------------
proc findReferencingRows(ctx: ExecutionContext, childTable: string, fkCol: string, fkValue: string): seq[Row] =
result = @[]
for row in execScan(ctx, childTable):
if fkCol in row and valueToString(row[fkCol]) == fkValue:
result.add(row)
proc enforceFkOnDelete(ctx: ExecutionContext, parentTable: string, parentCol: string, parentVal: string): (bool, string) =
for childTblName, childTbl in ctx.tables:
for col in childTbl.columns:
if col.fkTable == parentTable and col.fkColumn == parentCol:
let action = if col.fkOnDelete.len > 0: col.fkOnDelete else: "RESTRICT"
let refs = findReferencingRows(ctx, childTblName, col.name, parentVal)
if refs.len > 0:
case action
of "CASCADE":
for refRow in refs:
if "$key" in refRow:
var dummy: seq[(string, seq[byte])] = @[]
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])] = @[]
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)
return (true, "")
proc enforceFkOnUpdate(ctx: ExecutionContext, parentTable: string, parentCol: string, oldVal: string, newVal: string): (bool, string) =
for childTblName, childTbl in ctx.tables:
for col in childTbl.columns:
if col.fkTable == parentTable and col.fkColumn == parentCol:
let action = if col.fkOnUpdate.len > 0: col.fkOnUpdate else: "RESTRICT"
let refs = findReferencingRows(ctx, childTblName, col.name, oldVal)
if refs.len > 0:
case action
of "CASCADE":
for refRow in refs:
if "$key" in refRow:
var sets = initTable[string, string]()
sets[col.name] = newVal
var dummy: seq[(string, seq[byte])] = @[]
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])] = @[]
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)
return (true, "")
proc enforceFkOnChildUpdate(ctx: ExecutionContext, childTable: string, fkCol: string, newVal: string): (bool, string) =
let tbl = ctx.getTableDef(childTable)
var parentTable = ""
var parentCol = ""
for col in tbl.columns:
if col.name == fkCol:
parentTable = col.fkTable
parentCol = col.fkColumn
break
if parentTable.len == 0 or parentCol.len == 0:
return (true, "")
if isNull(newVal):
return (true, "")
let fkKey = parentTable & "." & parentCol & "=" & newVal
let (fkExists, _) = ctx.db.get(fkKey)
if fkExists:
return (true, "")
var found = false
let prefix = parentTable & "."
for entry in ctx.db.scanMemTable():
if entry.deleted: continue
if entry.key.startsWith(prefix):
let rest = entry.key[prefix.len..^1]
if rest.startsWith(parentCol & "=") and rest[parentCol.len+1..^1] == newVal:
found = true
break
if not found:
return (false, "FOREIGN KEY violation: '" & newVal & "' not found in " & parentTable & "." & parentCol)
return (true, "")
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# Constraint Validation # Constraint Validation
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------