diff --git a/docs/changelog.rst b/docs/changelog.rst index 01d59a0..b182d88 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,19 @@ Breaking changes: - ``table.foreign_keys`` now returns ``ForeignKey`` objects that are dataclasses rather than ``namedtuple`` instances, so they can no longer be unpacked or indexed as ``(table, column, other_table, other_column)`` tuples - access their fields by name instead. Compound (multi-column) foreign keys are now represented as a single ``ForeignKey`` with ``is_compound=True`` and populated ``columns``/``other_columns`` lists, where ``column`` and ``other_column`` are ``None``. Previously they were returned as one ``ForeignKey`` per column, misleadingly suggesting several independent foreign keys. See :ref:`upgrading_3_to_4` for details. (:issue:`594`) +Compound foreign key support, everywhere: + +- Tables can now be created with compound foreign keys, by passing lists of column names in ``foreign_keys=``: ``foreign_keys=[(["campus_name", "dept_code"], "departments")]``. The referenced columns default to the compound primary key of the other table. Compound keys are rendered as table-level ``FOREIGN KEY`` constraints in the generated schema. See :ref:`python_api_compound_foreign_keys`. +- ``table.transform()`` now preserves compound foreign keys, applying any column renames to them. Dropping a column that is part of a compound foreign key drops the whole constraint, matching the existing single-column behavior. ``drop_foreign_keys=`` accepts a bare column name - dropping any foreign key that column participates in - or a tuple of columns to target a compound key precisely. +- ``table.add_foreign_key()`` and ``db.add_foreign_keys()`` accept lists of column names to add a compound foreign key to an existing table. +- ``db.index_foreign_keys()`` creates a single composite index for a compound foreign key. + +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. +- 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. + .. _v4_0rc2: 4.0rc2 (2026-07-04) diff --git a/docs/python-api.rst b/docs/python-api.rst index f44a10b..9c31841 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -2309,8 +2309,8 @@ Each ``ForeignKey`` has the following attributes: :: >>> db.table("Street_Tree_List").foreign_keys - [ForeignKey(table='Street_Tree_List', column='qLegalStatus', other_table='qLegalStatus', other_column='id', columns=['qLegalStatus'], other_columns=['id'], is_compound=False), - ForeignKey(table='Street_Tree_List', column='qCareAssistant', other_table='qCareAssistant', other_column='id', columns=['qCareAssistant'], other_columns=['id'], is_compound=False), + [ForeignKey(table='Street_Tree_List', column='qLegalStatus', other_table='qLegalStatus', other_column='id', columns=['qLegalStatus'], other_columns=['id'], is_compound=False, on_delete='NO ACTION', on_update='NO ACTION'), + ForeignKey(table='Street_Tree_List', column='qCareAssistant', other_table='qCareAssistant', other_column='id', columns=['qCareAssistant'], other_columns=['id'], is_compound=False, on_delete='NO ACTION', on_update='NO ACTION'), ...] Compound foreign keys - defined with ``FOREIGN KEY (col_a, col_b) REFERENCES other(col_a, col_b)`` - are returned as a single ``ForeignKey`` with ``is_compound=True``, ``column`` and ``other_column`` set to ``None``, and the participating columns available in the ``columns`` and ``other_columns`` lists. diff --git a/docs/reference.rst b/docs/reference.rst index 5b5fd25..a9fdf29 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -70,6 +70,13 @@ sqlite_utils.db.ColumnDetails .. autoclass:: sqlite_utils.db.ColumnDetails +.. _reference_db_other_foreign_key: + +sqlite_utils.db.ForeignKey +-------------------------- + +.. autoclass:: sqlite_utils.db.ForeignKey + sqlite_utils.utils ================== diff --git a/docs/upgrading.rst b/docs/upgrading.rst index eb997f6..67a6b5e 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -79,7 +79,7 @@ Python API changes **View.enable_fts() has been removed.** The ``View`` class previously had an ``enable_fts()`` method that existed only to raise ``NotImplementedError`` - full-text search is not supported for views. Calling it now raises ``AttributeError`` like any other missing method. -**ForeignKey is now a dataclass, not a namedtuple.** The ``ForeignKey`` objects returned by ``table.foreign_keys`` gained three new fields - ``columns``, ``other_columns`` and ``is_compound`` - so that compound (multi-column) foreign keys can be represented as a single object. To make room for those fields cleanly ``ForeignKey`` is now a dataclass rather than a ``namedtuple``, so it can no longer be unpacked or indexed as a tuple. Access its fields by name instead: +**ForeignKey is now a dataclass, not a namedtuple.** The ``ForeignKey`` objects returned by ``table.foreign_keys`` gained new fields - ``columns``, ``other_columns``, ``is_compound``, ``on_delete`` and ``on_update`` - so that compound (multi-column) foreign keys and foreign key actions can be represented. To make room for those fields cleanly ``ForeignKey`` is now a dataclass rather than a ``namedtuple``, so it can no longer be unpacked or indexed as a tuple. Access its fields by name instead: .. code-block:: python @@ -95,6 +95,8 @@ Attempting the old unpacking or ``fk[0]`` indexing now raises ``TypeError``, so Compound foreign keys - previously returned as one ``ForeignKey`` per column, misleadingly suggesting several independent single-column keys - are now returned as a single ``ForeignKey`` with ``is_compound=True``. For these the scalar ``column`` and ``other_column`` fields are ``None``; use the ``columns`` and ``other_columns`` lists instead. Single-column foreign keys are unaffected apart from the class change: ``column``/``other_column`` behave as before and ``columns``/``other_columns`` are single-item lists. +Two related behavior changes to ``table.transform()``: compound foreign keys now survive a transform (previously they were split into separate single-column keys), and ``ON DELETE``/``ON UPDATE`` actions such as ``ON DELETE CASCADE`` are now preserved (previously they were silently stripped from the schema). + **Validation errors raise ValueError.** Invalid arguments to Python API methods - for example ``create_table()`` with no columns, or ``ignore=True`` together with ``replace=True`` - now raise ``ValueError``. They previously raised ``AssertionError`` from bare ``assert`` statements, which were silently skipped under ``python -O``. **Transaction behavior is now well-defined.** 4.0 introduces the :ref:`db.atomic() ` context manager and uses it consistently for every write operation - the full model is described in :ref:`python_api_transactions`. Changes you may notice: diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index dbb7445..009bafc 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -176,6 +176,9 @@ class ForeignKey: are ``None`` - use ``columns`` and ``other_columns`` instead, and check ``is_compound``. + ``on_delete`` and ``on_update`` hold the foreign key actions, e.g. + ``"CASCADE"`` - ``"NO ACTION"`` if not set. + Prior to sqlite-utils 4.0 this was a ``namedtuple`` and could be unpacked or indexed as ``(table, column, other_table, other_column)``. It is now a dataclass - access its fields by name instead.