diff --git a/docs/changelog.rst b/docs/changelog.rst index cacd5a0..f6a8ea2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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. diff --git a/docs/python-api.rst b/docs/python-api.rst index 9ff89eb..a33b256 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -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 diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 7379859..3f23c7d 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -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: diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index 7fcd788..8950189 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -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