feat(fk): Foreign Key Enforcement — ON DELETE/UPDATE CASCADE, SET NULL, RESTRICT
- Add tkRestrict token to lexer - Parse ON DELETE CASCADE/SET NULL/RESTRICT and ON UPDATE CASCADE/SET NULL/RESTRICT in both table-level and column-level FK constraints - Add fkOnDelete/fkOnUpdate to ColumnDef and ForeignKeyDef - Fix table-level FK constraint application (third pass after columns are created) - Implement enforceFkOnDelete, enforceFkOnUpdate, enforceFkOnChildUpdate helpers - Wire FK enforcement into DELETE and UPDATE execution paths - Add 9 regression tests covering all FK actions
This commit is contained in:
@@ -92,6 +92,7 @@ type
|
||||
refTable*: string
|
||||
refColumn*: string
|
||||
onDelete*: string # CASCADE, SET NULL, RESTRICT
|
||||
onUpdate*: string # CASCADE, SET NULL, RESTRICT
|
||||
|
||||
CheckDef* = object
|
||||
name*: string
|
||||
@@ -121,6 +122,8 @@ type
|
||||
defaultVal*: string
|
||||
fkTable*: string
|
||||
fkColumn*: string
|
||||
fkOnDelete*: string
|
||||
fkOnUpdate*: string
|
||||
autoIncrement*: bool
|
||||
|
||||
Row* = Table[string, string]
|
||||
@@ -314,6 +317,8 @@ proc restoreSchema(ctx: ExecutionContext) =
|
||||
of "fkey":
|
||||
colDef.fkTable = cst.cstRefTable
|
||||
colDef.fkColumn = if cst.cstRefColumns.len > 0: cst.cstRefColumns[0] else: ""
|
||||
colDef.fkOnDelete = cst.cstOnDelete
|
||||
colDef.fkOnUpdate = cst.cstOnUpdate
|
||||
else: discard
|
||||
tbl.columns.add(colDef)
|
||||
ctx.tables[stmt.crtName] = tbl
|
||||
@@ -1436,6 +1441,96 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
|
||||
vengine.insert(vecIdx, docId, vec, meta)
|
||||
return 1
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 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 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, 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, 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, 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, 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
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -3656,6 +3751,22 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
updValues.add("\\N")
|
||||
let (valid, errMsg) = validateConstraints(ctx, stmt.updTarget, updFields, @[updValues], skipPkCheck = true)
|
||||
if not valid: return errResult(errMsg)
|
||||
# FK ON UPDATE enforcement (parent side)
|
||||
var refCols: seq[string] = @[]
|
||||
for _, childTbl in ctx.tables:
|
||||
for col in childTbl.columns:
|
||||
if col.fkTable == stmt.updTarget and col.fkColumn notin refCols:
|
||||
refCols.add(col.fkColumn)
|
||||
for refCol in refCols:
|
||||
if refCol in sets and refCol in row:
|
||||
let (fkOk, fkErr) = enforceFkOnUpdate(ctx, stmt.updTarget, refCol, row[refCol], sets[refCol])
|
||||
if not fkOk:
|
||||
return errResult(fkErr)
|
||||
# FK ON UPDATE enforcement (child side — validate new FK values)
|
||||
for colName, newVal in sets:
|
||||
let (fkOk, fkErr) = enforceFkOnChildUpdate(ctx, stmt.updTarget, colName, newVal)
|
||||
if not fkOk:
|
||||
return errResult(fkErr)
|
||||
# Fire BEFORE UPDATE triggers
|
||||
var oldRow = row
|
||||
var newRow = row
|
||||
@@ -3686,6 +3797,18 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
# Fire BEFORE DELETE triggers
|
||||
fireTriggers(ctx, stmt.delTarget, "before", "delete", row)
|
||||
|
||||
# FK ON DELETE enforcement
|
||||
var refCols: seq[string] = @[]
|
||||
for _, childTbl in ctx.tables:
|
||||
for col in childTbl.columns:
|
||||
if col.fkTable == stmt.delTarget and col.fkColumn notin refCols:
|
||||
refCols.add(col.fkColumn)
|
||||
for refCol in refCols:
|
||||
if refCol in row:
|
||||
let (fkOk, fkErr) = enforceFkOnDelete(ctx, stmt.delTarget, refCol, row[refCol])
|
||||
if not fkOk:
|
||||
return errResult(fkErr)
|
||||
|
||||
count += execDelete(ctx, stmt.delTarget, row["$key"], kvPairs)
|
||||
|
||||
# Fire AFTER DELETE triggers
|
||||
@@ -3781,12 +3904,15 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
tbl.foreignKeys.add(ForeignKeyDef(
|
||||
refTable: cstNode.cstRefTable,
|
||||
refColumn: if cstNode.cstRefColumns.len > 0: cstNode.cstRefColumns[0] else: "",
|
||||
onDelete: cstNode.cstOnDelete))
|
||||
onDelete: cstNode.cstOnDelete,
|
||||
onUpdate: cstNode.cstOnUpdate))
|
||||
if cstNode.cstColumns.len > 0:
|
||||
for i, c in tbl.columns:
|
||||
if c.name in cstNode.cstColumns:
|
||||
tbl.columns[i].fkTable = cstNode.cstRefTable
|
||||
tbl.columns[i].fkColumn = if cstNode.cstRefColumns.len > 0: cstNode.cstRefColumns[0] else: ""
|
||||
tbl.columns[i].fkOnDelete = cstNode.cstOnDelete
|
||||
tbl.columns[i].fkOnUpdate = cstNode.cstOnUpdate
|
||||
elif cstNode.cstType == "check":
|
||||
tbl.checks.add(CheckDef(name: "check_" & $tbl.checks.len, checkNode: cstNode.cstCheck))
|
||||
|
||||
@@ -3815,10 +3941,27 @@ proc executeQueryImpl(ctx: ExecutionContext, astNode: Node, params: seq[WireValu
|
||||
of "fkey":
|
||||
colDef.fkTable = cst.cstRefTable
|
||||
colDef.fkColumn = if cst.cstRefColumns.len > 0: cst.cstRefColumns[0] else: ""
|
||||
colDef.fkOnDelete = cst.cstOnDelete
|
||||
colDef.fkOnUpdate = cst.cstOnUpdate
|
||||
of "check":
|
||||
tbl.checks.add(CheckDef(name: "check_" & col.cdName, checkNode: cst.cstCheck))
|
||||
else: discard
|
||||
tbl.columns.add(colDef)
|
||||
# Third pass: apply table-level constraints to columns
|
||||
for cstNode in stmt.crtConstraints:
|
||||
if cstNode.kind == nkConstraintDef:
|
||||
if cstNode.cstType == "pkey":
|
||||
for i, c in tbl.columns:
|
||||
if c.name in cstNode.cstColumns:
|
||||
tbl.columns[i].isPk = true
|
||||
elif cstNode.cstType == "fkey":
|
||||
if cstNode.cstColumns.len > 0:
|
||||
for i, c in tbl.columns:
|
||||
if c.name in cstNode.cstColumns:
|
||||
tbl.columns[i].fkTable = cstNode.cstRefTable
|
||||
tbl.columns[i].fkColumn = if cstNode.cstRefColumns.len > 0: cstNode.cstRefColumns[0] else: ""
|
||||
tbl.columns[i].fkOnDelete = cstNode.cstOnDelete
|
||||
tbl.columns[i].fkOnUpdate = cstNode.cstOnUpdate
|
||||
ctx.tables[stmt.crtName] = tbl
|
||||
|
||||
# Persist schema
|
||||
|
||||
@@ -85,6 +85,7 @@ type
|
||||
tkForeign
|
||||
tkReferences
|
||||
tkCascade
|
||||
tkRestrict
|
||||
tkUnique
|
||||
tkCheck
|
||||
tkDefault
|
||||
@@ -308,6 +309,7 @@ const keywords*: Table[string, TokenKind] = {
|
||||
"foreign": tkForeign,
|
||||
"references": tkReferences,
|
||||
"cascade": tkCascade,
|
||||
"restrict": tkRestrict,
|
||||
"unique": tkUnique,
|
||||
"check": tkCheck,
|
||||
"default": tkDefault,
|
||||
|
||||
@@ -984,6 +984,40 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnDelete = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnDelete = "RESTRICT"
|
||||
elif p.peek().kind == tkUpdate:
|
||||
discard p.advance()
|
||||
if p.peek().kind == tkCascade:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "CASCADE"
|
||||
elif p.peek().kind == tkSet:
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnUpdate = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "RESTRICT"
|
||||
elif p.peek().kind == tkSet:
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnDelete = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnDelete = "RESTRICT"
|
||||
elif p.peek().kind == tkUpdate:
|
||||
discard p.advance()
|
||||
if p.peek().kind == tkCascade:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "CASCADE"
|
||||
elif p.peek().kind == tkSet:
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnUpdate = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "RESTRICT"
|
||||
elif p.match(tkUnique):
|
||||
cst.cstType = "unique"
|
||||
if p.peek().kind == tkLParen:
|
||||
@@ -1031,7 +1065,7 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
colDef.cdType = "BIGINT"
|
||||
|
||||
# Parse column constraints
|
||||
while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences, tkAutoIncrement}:
|
||||
while p.peek().kind in {tkPrimary, tkNot, tkNull, tkUnique, tkCheck, tkDefault, tkReferences, tkAutoIncrement, tkOn}:
|
||||
let cst = Node(kind: nkConstraintDef)
|
||||
cst.cstColumns = @[colName]; cst.cstRefColumns = @[]
|
||||
if p.match(tkPrimary):
|
||||
@@ -1071,6 +1105,25 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
if p.peek().kind == tkCascade:
|
||||
discard p.advance()
|
||||
cst.cstOnDelete = "CASCADE"
|
||||
elif p.peek().kind == tkSet:
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnDelete = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnDelete = "RESTRICT"
|
||||
elif p.peek().kind == tkUpdate:
|
||||
discard p.advance()
|
||||
if p.peek().kind == tkCascade:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "CASCADE"
|
||||
elif p.peek().kind == tkSet:
|
||||
discard p.advance()
|
||||
discard p.match(tkNull)
|
||||
cst.cstOnUpdate = "SET NULL"
|
||||
elif p.peek().kind == tkRestrict:
|
||||
discard p.advance()
|
||||
cst.cstOnUpdate = "RESTRICT"
|
||||
colDef.cdConstraints.add(cst)
|
||||
result.crtColumns.add(colDef)
|
||||
|
||||
|
||||
@@ -3731,3 +3731,115 @@ suite "Join Performance — Hash Join & Index Nested Loop":
|
||||
check r.rows[3]["total"] == "\\N"
|
||||
|
||||
|
||||
|
||||
suite "Foreign Key Enforcement":
|
||||
var db: LSMTree
|
||||
var ctx: qexec.ExecutionContext
|
||||
var tmpDir: string
|
||||
|
||||
setup:
|
||||
tmpDir = getTempDir() / "baradb_fk_test_" & $getMonoTime().ticks
|
||||
db = newLSMTree(tmpDir)
|
||||
ctx = qexec.newExecutionContext(db)
|
||||
|
||||
teardown:
|
||||
removeDir(tmpDir)
|
||||
|
||||
test "ON DELETE CASCADE removes child rows":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id) ON DELETE CASCADE)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child (id, parent_id) VALUES (10, 1)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child (id, parent_id) VALUES (20, 1)"))
|
||||
let del = qexec.executeQuery(ctx, parse("DELETE FROM parent WHERE id = 1"))
|
||||
check del.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child"))
|
||||
check childSel.success
|
||||
check childSel.rows.len == 0
|
||||
|
||||
test "ON DELETE SET NULL sets FK to NULL":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent2 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child2 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent2(id) ON DELETE SET NULL)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent2 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child2 (id, parent_id) VALUES (10, 1)"))
|
||||
let del = qexec.executeQuery(ctx, parse("DELETE FROM parent2 WHERE id = 1"))
|
||||
check del.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child2"))
|
||||
check childSel.success
|
||||
check childSel.rows.len == 1
|
||||
check childSel.rows[0]["parent_id"] == "\\N"
|
||||
|
||||
test "ON DELETE RESTRICT blocks delete when referenced":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent3 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child3 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent3(id) ON DELETE RESTRICT)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent3 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child3 (id, parent_id) VALUES (10, 1)"))
|
||||
let del = qexec.executeQuery(ctx, parse("DELETE FROM parent3 WHERE id = 1"))
|
||||
check not del.success
|
||||
check del.message.contains("FOREIGN KEY violation")
|
||||
let parentSel = qexec.executeQuery(ctx, parse("SELECT * FROM parent3"))
|
||||
check parentSel.rows.len == 1
|
||||
|
||||
test "ON UPDATE CASCADE updates child rows":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent4 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child4 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent4(id) ON UPDATE CASCADE)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent4 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child4 (id, parent_id) VALUES (10, 1)"))
|
||||
let upd = qexec.executeQuery(ctx, parse("UPDATE parent4 SET id = 99 WHERE id = 1"))
|
||||
check upd.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child4"))
|
||||
check childSel.success
|
||||
check childSel.rows.len == 1
|
||||
check childSel.rows[0]["parent_id"] == "99"
|
||||
|
||||
test "ON UPDATE SET NULL sets FK to NULL":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent5 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child5 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent5(id) ON UPDATE SET NULL)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent5 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child5 (id, parent_id) VALUES (10, 1)"))
|
||||
let upd = qexec.executeQuery(ctx, parse("UPDATE parent5 SET id = 99 WHERE id = 1"))
|
||||
check upd.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child5"))
|
||||
check childSel.success
|
||||
check childSel.rows.len == 1
|
||||
check childSel.rows[0]["parent_id"] == "\\N"
|
||||
|
||||
test "ON UPDATE RESTRICT blocks update when referenced":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent6 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child6 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent6(id) ON UPDATE RESTRICT)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent6 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child6 (id, parent_id) VALUES (10, 1)"))
|
||||
let upd = qexec.executeQuery(ctx, parse("UPDATE parent6 SET id = 99 WHERE id = 1"))
|
||||
check not upd.success
|
||||
check upd.message.contains("FOREIGN KEY violation")
|
||||
|
||||
test "UPDATE child with valid FK value succeeds":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent7 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child7 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent7(id))"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent7 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent7 (id, name) VALUES (2, 'Bob')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child7 (id, parent_id) VALUES (10, 1)"))
|
||||
let upd = qexec.executeQuery(ctx, parse("UPDATE child7 SET parent_id = 2 WHERE id = 10"))
|
||||
check upd.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child7"))
|
||||
check childSel.rows[0]["parent_id"] == "2"
|
||||
|
||||
test "UPDATE child with invalid FK value fails":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent8 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child8 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent8(id))"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent8 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child8 (id, parent_id) VALUES (10, 1)"))
|
||||
let upd = qexec.executeQuery(ctx, parse("UPDATE child8 SET parent_id = 999 WHERE id = 10"))
|
||||
check not upd.success
|
||||
check upd.message.contains("FOREIGN KEY violation")
|
||||
|
||||
test "Table-level FK with ON DELETE CASCADE":
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE parent9 (id INTEGER PRIMARY KEY, name TEXT)"))
|
||||
discard qexec.executeQuery(ctx, parse("CREATE TABLE child9 (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent9(id) ON DELETE CASCADE)"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO parent9 (id, name) VALUES (1, 'Alice')"))
|
||||
discard qexec.executeQuery(ctx, parse("INSERT INTO child9 (id, parent_id) VALUES (10, 1)"))
|
||||
let del = qexec.executeQuery(ctx, parse("DELETE FROM parent9 WHERE id = 1"))
|
||||
check del.success
|
||||
let childSel = qexec.executeQuery(ctx, parse("SELECT * FROM child9"))
|
||||
check childSel.rows.len == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user