Capture and preserve foreign key ON DELETE/ON UPDATE actions

ForeignKey gains on_delete and on_update fields (default "NO ACTION"),
populated from PRAGMA foreign_key_list. create_table_sql() renders the
corresponding clauses for both inline and compound table-level foreign
keys, which means table.transform() now preserves actions such as
ON DELETE CASCADE - previously they were silently stripped whenever a
table was transformed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-07-05 14:40:57 -07:00
commit be27a96484
3 changed files with 139 additions and 10 deletions

View file

@ -190,6 +190,8 @@ class ForeignKey:
columns: List[str] = field(default_factory=list)
other_columns: List[str] = field(default_factory=list)
is_compound: bool = False
on_delete: str = "NO ACTION"
on_update: str = "NO ACTION"
def __post_init__(self):
# Populate columns/other_columns for single-column foreign keys
@ -201,6 +203,16 @@ class ForeignKey:
)
def _fk_actions_sql(fk: ForeignKey) -> str:
"ON UPDATE/ON DELETE clauses for a foreign key, or an empty string."
actions = ""
if fk.on_update and fk.on_update != "NO ACTION":
actions += " ON UPDATE {}".format(fk.on_update)
if fk.on_delete and fk.on_delete != "NO ACTION":
actions += " ON DELETE {}".format(fk.on_delete)
return actions
Index = namedtuple("Index", ("seq", "name", "unique", "origin", "partial", "columns"))
XIndex = namedtuple("XIndex", ("name", "columns"))
XIndexColumn = namedtuple(
@ -1345,14 +1357,12 @@ class Database:
"DEFAULT {}".format(self.quote_default_value(defaults[column_name]))
)
if column_name in foreign_keys_by_column:
fk = foreign_keys_by_column[column_name]
column_extras.append(
"REFERENCES {}({})".format(
quote_identifier(
foreign_keys_by_column[column_name].other_table
),
quote_identifier(
cast(str, foreign_keys_by_column[column_name].other_column)
),
"REFERENCES {}({}){}".format(
quote_identifier(fk.other_table),
quote_identifier(cast(str, fk.other_column)),
_fk_actions_sql(fk),
)
)
column_type_str = COLUMN_TYPE_MAPPING[column_type]
@ -1385,12 +1395,13 @@ class Database:
"No such column: {}".format(", ".join(sorted(missing)))
)
column_defs.append(
" FOREIGN KEY ({columns}) REFERENCES {other_table}({other_columns})".format(
" FOREIGN KEY ({columns}) REFERENCES {other_table}({other_columns}){actions}".format(
columns=", ".join(quote_identifier(c) for c in fk.columns),
other_table=quote_identifier(fk.other_table),
other_columns=", ".join(
quote_identifier(c) for c in fk.other_columns
),
actions=_fk_actions_sql(fk),
)
)
columns_sql = ",\n".join(column_defs)
@ -2055,7 +2066,9 @@ class Table(Queryable):
).fetchall():
if row is not None:
id, seq, table_name, from_, to_, on_update, on_delete, match = row
by_id.setdefault(id, []).append((seq, table_name, from_, to_))
by_id.setdefault(id, []).append(
(seq, table_name, from_, to_, on_update, on_delete)
)
fks = []
for id in sorted(by_id):
rows = sorted(by_id[id]) # order columns by seq
@ -2072,6 +2085,8 @@ class Table(Queryable):
columns=columns,
other_columns=other_columns,
is_compound=is_compound,
on_update=rows[0][4],
on_delete=rows[0][5],
)
)
return fks
@ -2434,9 +2449,16 @@ class Table(Queryable):
columns=columns,
other_columns=fk.other_columns,
is_compound=True,
on_delete=fk.on_delete,
on_update=fk.on_update,
)
return ForeignKey(
self.name, columns[0], fk.other_table, fk.other_columns[0]
self.name,
columns[0],
fk.other_table,
fk.other_columns[0],
on_delete=fk.on_delete,
on_update=fk.on_update,
)
create_table_foreign_keys = []