Session: zero data when foreign key conflict in apply
(1) By Roger Binns (rogerbinns) on 2025-05-06 23:03:22 [source]
I have a changeset that consists of a single delete operation. If that row is deleted then it would cause a foreign key violation because another table is referring to the row.
The problem is that the conflict handler is getting "zero" data for the changeset_iter contents, contradicting the documentation.
sqlite3changeset_op is returning SQLITE_OK
, and setting number of columns to 1 (correct), the operation to 0 (incorrect, not one of the constants), and the table name to NULL (incorrect).
The problem is the session code has a zeroed iter that it is passing. It looks like the code is wrong and should be providing more useful information like a correct table name, operation etc.
Line number from 3.49.1 amalgamation is sessionChangesetApply sqlite3/sqlite3.c:232099
(gdb) frame 19
#19 0x00007fffe95c3120 in sessionChangesetApply (db=0xbb23e0, pIter=0xc29870, xFilter=0x0, xConflict=0x7fffe9669eb3 <applyConflict>, pCtx=0x7fffffffce20, ppRebase=0x0, pnRebase=0x0, flags=0) at /space/apsw/sqlite3/sqlite3.c:232099
232099 res = xConflict(pCtx, SQLITE_CHANGESET_FOREIGN_KEY, &sIter);
(gdb) p sIter
$1 = {in = {bNoDiscard = 0, iCurrent = 0, iNext = 0, aData = 0x0, nData = 0, buf = {aBuf = 0x0, nBuf = 0, nAlloc = 0}, xInput = 0x0, pIn = 0x0, bEof = 0}, tblhdr = {aBuf = 0x0, nBuf = 0, nAlloc = 0}, bPatchset = 0, bInvert = 0, bSkipEmpty = 0, rc = 0, pConflict = 0x0, zTab = 0x0, nCol = 1, op = 0, bIndirect = 0,
abPK = 0x0, apValue = 0x0}
(gdb) p *pIter
$2 = {in = {bNoDiscard = 3113, iCurrent = 0, iNext = 1569516194, aData = 0x7fffe98da590 "T\001\001t1", nData = 17, buf = {aBuf = 0x0, nBuf = 0, nAlloc = 0}, xInput = 0x0, pIn = 0x0, bEof = 1}, tblhdr = {aBuf = 0xbb2700 "\362\232", <incomplete sequence \302>, nBuf = 0, nAlloc = 256}, bPatchset = 0, bInvert = 0,
bSkipEmpty = 1, rc = 0, pConflict = 0x0, zTab = 0xbb2711 "t1", nCol = 1, op = 9, bIndirect = 0, abPK = 0xbb2710 "\001t1", apValue = 0xbb2700}
The following Python code reproduces the issue. The conflict handler prints 5 (SQLITE_CHANGESET_FOREIGN_KEY) as the reason, and then my code doesn't know how to handle the zero op code etc.
import apsw
db = apsw.Connection("")
db.pragma("foreign_keys", "on")
db.execute("create table t1(one PRIMARY KEY); insert into t1 values(1)")
session = apsw.Session(db, "main")
session.attach("t1")
db.execute("delete from t1")
changeset = session.changeset()
# changeset now contains delete of 1 row
# puts the row back, adds another table with a row referencing
# the changeset row
db.execute("""
insert into t1 values(1);
create table t2(one, FOREIGN KEY(one) REFERENCES t1(one));
insert into t2 values(1);
""")
def handler(reason, change):
# prints 5
print(f"{reason=}")
# assertion failures in my code due to undocumented values
print(change)
return apsw.SQLITE_CHANGESET_OMIT
apsw.Changeset.apply(changeset, db, conflict=handler)