add_foreign_key() on_delete= and on_update= parameters, closes #530

table.add_foreign_key() now accepts on_delete= and on_update= to
create foreign keys with ON DELETE/ON UPDATE actions:

    table.add_foreign_key("author_id", "authors", "id", on_delete="CASCADE")

Works for compound foreign keys too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-07-05 15:49:20 -07:00
commit 0ec0180405
4 changed files with 61 additions and 3 deletions

View file

@ -23,6 +23,7 @@ Compound foreign key support:
Other foreign key improvements:
- ``ForeignKey`` now exposes ``on_delete`` and ``on_update`` fields reflecting the foreign key's ``ON DELETE``/``ON UPDATE`` actions, and ``table.transform()`` preserves those actions. Previously a transform silently stripped clauses such as ``ON DELETE CASCADE`` from the table schema.
- ``table.add_foreign_key()`` accepts new ``on_delete=`` and ``on_update=`` parameters for creating foreign keys with actions, e.g. ``table.add_foreign_key("author_id", "authors", "id", on_delete="CASCADE")``. (:issue:`530`)
- Foreign keys declared as ``REFERENCES other_table`` with no explicit column are now resolved to the other table's primary key by ``table.foreign_keys``, instead of reporting ``other_column=None``.
- Fixed a ``TypeError`` when sorting ``ForeignKey`` objects where some were compound.

View file

@ -1591,6 +1591,16 @@ To add a compound foreign key, pass tuples of columns:
As with single columns, omitting the other columns will use the compound primary key of the other table. ``other_table`` must always be specified for a compound foreign key.
Use ``on_delete=`` and ``on_update=`` to specify ``ON DELETE`` and ``ON UPDATE`` actions for the foreign key:
.. code-block:: python
db.table("books").add_foreign_key(
"author_id", "authors", "id", on_delete="CASCADE"
)
This creates a foreign key with an ``ON DELETE CASCADE`` clause, so deleting an author will also delete their books (provided foreign key enforcement is enabled with ``PRAGMA foreign_keys = ON``). Valid actions are ``"SET NULL"``, ``"SET DEFAULT"``, ``"CASCADE"``, ``"RESTRICT"`` and the default ``"NO ACTION"``.
.. _python_api_add_foreign_keys:
Adding multiple foreign key constraints at once

View file

@ -2934,6 +2934,8 @@ class Table(Queryable):
other_table: Optional[str] = None,
other_column: Optional[ForeignKeyColumns] = None,
ignore: bool = False,
on_delete: str = "NO ACTION",
on_update: str = "NO ACTION",
):
"""
Alter the schema to mark the specified column as a foreign key to another table.
@ -2944,6 +2946,9 @@ class Table(Queryable):
:param other_column: The column on the other table it - if omitted, will be guessed.
Use a tuple of columns for a compound foreign key.
:param ignore: Set this to ``True`` to ignore an existing foreign key - otherwise a ``AlterError`` will be raised.
:param on_delete: ``ON DELETE`` action for the foreign key, e.g. ``"CASCADE"``
or ``"SET NULL"``.
:param on_update: ``ON UPDATE`` action for the foreign key.
"""
columns = (column,) if isinstance(column, str) else tuple(column)
# Ensure columns exist
@ -2997,11 +3002,27 @@ class Table(Queryable):
)
)
if len(columns) == 1:
self.db.add_foreign_keys(
[(self.name, columns[0], other_table, other_columns[0])]
fk_object = ForeignKey(
self.name,
columns[0],
other_table,
other_columns[0],
on_delete=on_delete,
on_update=on_update,
)
else:
self.db.add_foreign_keys([(self.name, columns, other_table, other_columns)])
fk_object = ForeignKey(
self.name,
None,
other_table,
None,
columns=columns,
other_columns=other_columns,
is_compound=True,
on_delete=on_delete,
on_update=on_update,
)
self.db.add_foreign_keys([fk_object])
return self
def enable_counts(self) -> None:

View file

@ -498,3 +498,29 @@ def test_add_foreign_keys_preserves_actions_compound(courses_db):
assert fk.is_compound is True
assert fk.on_delete == "CASCADE"
assert "ON DELETE CASCADE" in courses_db["courses"].schema
def test_add_foreign_key_on_delete_on_update(fresh_db):
fresh_db["authors"].insert({"id": 1}, pk="id")
fresh_db["books"].insert({"id": 1, "author_id": 1}, pk="id")
fresh_db["books"].add_foreign_key(
"author_id", "authors", "id", on_delete="CASCADE", on_update="RESTRICT"
)
fk = fresh_db["books"].foreign_keys[0]
assert fk.on_delete == "CASCADE"
assert fk.on_update == "RESTRICT"
assert "ON UPDATE RESTRICT ON DELETE CASCADE" in fresh_db["books"].schema
# The cascade should actually fire
fresh_db.execute("PRAGMA foreign_keys = ON")
fresh_db.execute("delete from authors where id = 1")
assert fresh_db["books"].count == 0
def test_add_compound_foreign_key_on_delete(courses_db):
courses_db["courses"].add_foreign_key(
("campus_name", "dept_code"), "departments", on_delete="SET NULL"
)
fk = courses_db["courses"].foreign_keys[0]
assert fk.is_compound is True
assert fk.on_delete == "SET NULL"
assert "ON DELETE SET NULL" in courses_db["courses"].schema