From d100264e9c11f73c2f86328846a33e500eeb2118 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 5 Jul 2026 15:10:21 -0700 Subject: [PATCH] Normalize compound foreign key columns to tuples, promote tuples in docs ForeignKey.columns and .other_columns are now tuples rather than lists, both when introspected and when constructed directly (lists are normalized in __post_init__). Tuples are now the documented form for specifying compound foreign keys everywhere - in create_table() foreign_keys=, add_foreign_key() and drop_foreign_keys=: foreign_keys=[(("campus_name", "dept_code"), "departments")] Lists continue to work as input but are no longer documented. Co-Authored-By: Claude Fable 5 --- docs/changelog.rst | 8 ++-- docs/python-api.rst | 22 +++++------ docs/upgrading.rst | 2 +- sqlite_utils/db.py | 77 +++++++++++++++++++------------------- tests/test_foreign_keys.py | 73 +++++++++++++++++++----------------- 5 files changed, 92 insertions(+), 90 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b182d88..cacd5a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,13 +11,13 @@ Unreleased 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`) +- ``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`` tuples, 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: +Compound foreign key support: -- 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`. +- Tables can now be created with compound foreign keys, by passing tuples 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. +- ``table.add_foreign_key()`` and ``db.add_foreign_keys()`` accept tuples 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: diff --git a/docs/python-api.rst b/docs/python-api.rst index 9c31841..9ff89eb 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -822,7 +822,7 @@ You can leave off the third item in the tuple to have the referenced column auto Compound foreign keys ~~~~~~~~~~~~~~~~~~~~~ -To create a compound (multi-column) foreign key, use lists of column names in place of the single column names: +To create a compound (multi-column) foreign key, use tuples of column names in place of the single column names: .. code-block:: python @@ -831,7 +831,7 @@ To create a compound (multi-column) foreign key, use lists of column names in pl "campus_name": str, "dept_code": str, }, pk="course_code", foreign_keys=[ - (["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"]) + (("campus_name", "dept_code"), "departments", ("campus_name", "dept_code")) ]) This creates a table-level constraint: @@ -845,12 +845,12 @@ This creates a table-level constraint: FOREIGN KEY ("campus_name", "dept_code") REFERENCES "departments"("campus_name", "dept_code") ) -As with single columns, you can leave off the list of other columns to reference the compound primary key of the other table: +As with single columns, you can leave off the tuple of other columns to reference the compound primary key of the other table: .. code-block:: python foreign_keys=[ - (["campus_name", "dept_code"], "departments") + (("campus_name", "dept_code"), "departments") ] To specify ``ON DELETE`` or ``ON UPDATE`` actions, pass ``ForeignKey`` objects instead: @@ -1581,12 +1581,12 @@ To ignore the case where the key already exists, use ``ignore=True``: db.table("books").add_foreign_key("author_id", "authors", "id", ignore=True) -To add a compound foreign key, pass lists of columns: +To add a compound foreign key, pass tuples of columns: .. code-block:: python db.table("courses").add_foreign_key( - ["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"] + ("campus_name", "dept_code"), "departments", ("campus_name", "dept_code") ) 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. @@ -2294,9 +2294,9 @@ Each ``ForeignKey`` has the following attributes: ``other_column`` The referenced column, or ``None`` for a compound foreign key. ``columns`` - A list of the columns on this table, always populated (a single-item list for single-column foreign keys). + A tuple of the columns on this table, always populated (a one-item tuple for single-column foreign keys). ``other_columns`` - A list of the referenced columns. + A tuple of the referenced columns. ``is_compound`` ``True`` if this is a compound (multi-column) foreign key. ``on_delete`` @@ -2309,11 +2309,11 @@ 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, 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'), + [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. +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`` tuples. .. _python_api_introspection_schema: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 67a6b5e..e7fd273 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -93,7 +93,7 @@ Python API changes Attempting the old unpacking or ``fk[0]`` indexing now raises ``TypeError``, so any code using those patterns will fail loudly rather than silently misbehave. -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. +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`` tuples instead. Single-column foreign keys are unaffected apart from the class change: ``column``/``other_column`` behave as before and ``columns``/``other_columns`` are one-item tuples. 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). diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 4e67a3c..a739486 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -170,7 +170,7 @@ class ForeignKey: A foreign key defined on a table. For single-column foreign keys ``column`` and ``other_column`` hold the - column names, and ``columns``/``other_columns`` are single-item lists. + column names, and ``columns``/``other_columns`` are one-item tuples. For compound (multi-column) foreign keys ``column`` and ``other_column`` are ``None`` - use ``columns`` and ``other_columns`` instead, and check @@ -190,24 +190,24 @@ class ForeignKey: column: Optional[str] = field(compare=False) other_table: str other_column: Optional[str] = field(compare=False) - columns: List[str] = field(default_factory=list) - other_columns: List[str] = field(default_factory=list) + columns: Tuple[str, ...] = () + other_columns: Tuple[str, ...] = () 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, - # normalizing any tuples to lists + # normalizing any lists to tuples if self.columns: - self.columns = list(self.columns) + self.columns = tuple(self.columns) else: - self.columns = [self.column] if self.column is not None else [] + self.columns = (self.column,) if self.column is not None else () if self.other_columns: - self.other_columns = list(self.other_columns) + self.other_columns = tuple(self.other_columns) else: self.other_columns = ( - [self.other_column] if self.other_column is not None else [] + (self.other_column,) if self.other_column is not None else () ) @@ -233,8 +233,8 @@ class TransformError(Exception): pass -# A single column name, or a list/tuple of columns for a compound foreign key -ForeignKeyColumns = Union[str, List[str], Tuple[str, ...]] +# A single column name, or a tuple of columns for a compound foreign key +ForeignKeyColumns = Union[str, Tuple[str, ...], List[str]] # (table, column(s), other_table, other_column(s)) ForeignKeyTuple = Tuple[str, ForeignKeyColumns, str, ForeignKeyColumns] @@ -1171,9 +1171,9 @@ class Database: string, a ForeignKey() object, a tuple of (column, other_table), or a tuple of (column, other_table, other_column), or a tuple of (table, column, other_table, other_column). For compound foreign - keys the column elements can be lists of column names, e.g. - (["campus_name", "dept_code"], "departments") or - (["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"]) + keys the column elements can be tuples of column names, e.g. + (("campus_name", "dept_code"), "departments") or + (("campus_name", "dept_code"), "departments", ("campus_name", "dept_code")) """ table = self.table(name) if all(isinstance(fk, ForeignKey) for fk in foreign_keys): @@ -1207,18 +1207,17 @@ class Database: other_table = tuple_or_list[1] if isinstance(column_or_columns, (list, tuple)): # Compound foreign key - columns = list(column_or_columns) + columns = tuple(column_or_columns) if len(tuple_or_list) == 3: - other_columns = tuple_or_list[2] - if not isinstance(other_columns, (list, tuple)): + if not isinstance(tuple_or_list[2], (list, tuple)): raise ValueError( - "Compound foreign key {} should reference a list " + "Compound foreign key {} should reference a tuple " "of other columns".format(tuple(tuple_or_list)) ) - other_columns = list(other_columns) + other_columns = tuple(tuple_or_list[2]) else: # Guess the compound primary key of the other table - other_columns = self.table(other_table).pks + other_columns = tuple(self.table(other_table).pks) if len(columns) != len(other_columns): raise ValueError( "Compound foreign key {} should have the same number " @@ -1618,7 +1617,7 @@ class Database: :param foreign_keys: A list of ``(table, column, other_table, other_column)`` tuples - for compound foreign keys, ``column`` and ``other_column`` can - be lists of column names + be tuples of column names """ # foreign_keys is a list of explicit 4-tuples if not all( @@ -1644,16 +1643,16 @@ class Database: ) else: table, column_or_columns, other_table, other_column_or_columns = fk - # Compound foreign keys use lists of columns + # Compound foreign keys use tuples of columns columns = ( - [column_or_columns] + (column_or_columns,) if isinstance(column_or_columns, str) - else list(column_or_columns) + else tuple(column_or_columns) ) other_columns = ( - [other_column_or_columns] + (other_column_or_columns,) if isinstance(other_column_or_columns, str) - else list(other_column_or_columns) + else tuple(other_column_or_columns) ) if not self.table(table).exists(): raise AlterError("No such table: {}".format(table)) @@ -1708,9 +1707,9 @@ class Database: existing_indexes = {tuple(i.columns) for i in table.indexes} for fk in table.foreign_keys: # A compound foreign key gets a single composite index - if tuple(fk.columns) not in existing_indexes: + if fk.columns not in existing_indexes: table.create_index(fk.columns, find_unique_name=True) - existing_indexes.add(tuple(fk.columns)) + existing_indexes.add(fk.columns) def vacuum(self) -> None: "Run a SQLite ``VACUUM`` against the database." @@ -2083,12 +2082,12 @@ class Table(Queryable): for id in sorted(by_id): rows = sorted(by_id[id]) # order columns by seq other_table = rows[0][1] - columns = [row[2] for row in rows] - other_columns = [row[3] for row in rows] + columns = tuple(row[2] for row in rows) + other_columns = tuple(row[3] for row in rows) if all(c is None for c in other_columns): # "REFERENCES other_table" with no columns - the pragma # returns None, meaning the other table's primary key - other_table_pks = self.db.table(other_table).pks + other_table_pks = tuple(self.db.table(other_table).pks) if len(other_table_pks) == len(columns): other_columns = other_table_pks is_compound = len(rows) > 1 @@ -2448,14 +2447,14 @@ class Table(Queryable): # A column name matches any foreign key it participates in if spec in fk.columns: return True - elif list(spec) == fk.columns: + elif tuple(spec) == fk.columns: # A tuple/list must match a compound key's columns exactly return True # Dropping any of a foreign key's columns drops the whole key return any(column in drop for column in fk.columns) def fk_with_renamed_columns(fk: ForeignKey) -> ForeignKey: - columns = [rename.get(column) or column for column in fk.columns] + columns = tuple(rename.get(column) or column for column in fk.columns) if fk.is_compound: return ForeignKey( self.name, @@ -2926,14 +2925,14 @@ class Table(Queryable): """ Alter the schema to mark the specified column as a foreign key to another table. - :param column: The column to mark as a foreign key - use a list of columns + :param column: The column to mark as a foreign key - use a tuple of columns for a compound foreign key. :param other_table: The table it refers to - if omitted, will be guessed based on the column name. :param other_column: The column on the other table it - if omitted, will be guessed. - Use a list of columns for a compound foreign key. + 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. """ - columns = [column] if isinstance(column, str) else list(column) + columns = (column,) if isinstance(column, str) else tuple(column) # Ensure columns exist for col in columns: if col not in self.columns_dict: @@ -2948,13 +2947,13 @@ class Table(Queryable): # If other_column is not specified, detect the primary key on other_table if other_column is None: if len(columns) > 1: - other_columns = self.db.table(other_table).pks + other_columns = tuple(self.db.table(other_table).pks) else: - other_columns = [self.guess_foreign_column(other_table)] + other_columns = (self.guess_foreign_column(other_table),) elif isinstance(other_column, str): - other_columns = [other_column] + other_columns = (other_column,) else: - other_columns = list(other_column) + other_columns = tuple(other_column) if len(columns) != len(other_columns): raise ValueError( "Compound foreign key must have the same number of columns " diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index 54039b3..d1bc498 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -36,8 +36,8 @@ def test_compound_foreign_key(compound_db): assert fk.is_compound is True assert fk.table == "courses" assert fk.other_table == "departments" - assert fk.columns == ["campus_name", "dept_code"] - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") + assert fk.other_columns == ("campus_name", "dept_code") # Scalar column/other_column can't sensibly hold a compound key assert fk.column is None assert fk.other_column is None @@ -51,8 +51,8 @@ def test_single_foreign_key_gets_columns_fields(fresh_db): assert fk.is_compound is False assert fk.column == "author_id" assert fk.other_column == "id" - assert fk.columns == ["author_id"] - assert fk.other_columns == ["id"] + assert fk.columns == ("author_id",) + assert fk.other_columns == ("id",) def test_foreign_key_no_longer_unpacks_as_tuple(fresh_db): @@ -142,14 +142,16 @@ EXPECTED_COURSES_SCHEMA = ( column=None, other_table="departments", other_column=None, - columns=["campus_name", "dept_code"], - other_columns=["campus_name", "dept_code"], + columns=("campus_name", "dept_code"), + other_columns=("campus_name", "dept_code"), is_compound=True, ) ], - [(["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"])], + [(("campus_name", "dept_code"), "departments", ("campus_name", "dept_code"))], # Two-item form guesses the other table's primary key: - [(["campus_name", "dept_code"], "departments")], + [(("campus_name", "dept_code"), "departments")], + # Lists work too, though tuples are the documented form: + [(["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"])], ), ) def test_create_table_with_compound_foreign_key(departments_db, foreign_keys): @@ -164,9 +166,9 @@ def test_create_table_with_compound_foreign_key(departments_db, foreign_keys): assert len(fks) == 1 fk = fks[0] assert fk.is_compound is True - assert fk.columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") assert fk.other_table == "departments" - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") def test_create_table_compound_foreign_key_enforced(departments_db): @@ -175,7 +177,7 @@ def test_create_table_compound_foreign_key_enforced(departments_db): "courses", {"course_code": str, "campus_name": str, "dept_code": str}, pk="course_code", - foreign_keys=[(["campus_name", "dept_code"], "departments")], + foreign_keys=[(("campus_name", "dept_code"), "departments")], ) departments_db["departments"].insert( {"campus_name": "Berkeley", "dept_code": "CS", "dept_name": "Computer Science"} @@ -199,7 +201,7 @@ def test_create_table_compound_foreign_key_missing_other_column(departments_db): {"course_code": str, "campus_name": str, "dept_code": str}, pk="course_code", foreign_keys=[ - (["campus_name", "dept_code"], "departments", ["campus_name", "nope"]) + (("campus_name", "dept_code"), "departments", ("campus_name", "nope")) ], ) @@ -210,9 +212,9 @@ def test_transform_preserves_compound_foreign_key(compound_db): assert len(fks) == 1 fk = fks[0] assert fk.is_compound is True - assert fk.columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") assert fk.other_table == "departments" - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") def test_transform_rename_member_column_updates_compound_foreign_key(compound_db): @@ -221,9 +223,9 @@ def test_transform_rename_member_column_updates_compound_foreign_key(compound_db assert len(fks) == 1 fk = fks[0] assert fk.is_compound is True - assert fk.columns == ["campus", "dept_code"] + assert fk.columns == ("campus", "dept_code") # Referenced columns in the other table are unchanged - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") def test_transform_drop_member_column_drops_compound_foreign_key(compound_db): @@ -264,7 +266,7 @@ def courses_db(departments_db): def test_add_compound_foreign_key(courses_db): t = courses_db["courses"].add_foreign_key( - ["campus_name", "dept_code"], "departments", ["campus_name", "dept_code"] + ("campus_name", "dept_code"), "departments", ("campus_name", "dept_code") ) # Returns self assert t.name == "courses" @@ -272,33 +274,34 @@ def test_add_compound_foreign_key(courses_db): assert len(fks) == 1 fk = fks[0] assert fk.is_compound is True - assert fk.columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") assert fk.other_table == "departments" - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") def test_add_compound_foreign_key_guesses_other_columns(courses_db): + # Lists work here too, though tuples are the documented form courses_db["courses"].add_foreign_key(["campus_name", "dept_code"], "departments") fk = courses_db["courses"].foreign_keys[0] - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") def test_add_compound_foreign_key_error_if_already_exists(courses_db): - courses_db["courses"].add_foreign_key(["campus_name", "dept_code"], "departments") + courses_db["courses"].add_foreign_key(("campus_name", "dept_code"), "departments") with pytest.raises(AlterError) as ex: courses_db["courses"].add_foreign_key( - ["campus_name", "dept_code"], "departments" + ("campus_name", "dept_code"), "departments" ) assert "already exists" in ex.value.args[0] # ignore=True should not raise courses_db["courses"].add_foreign_key( - ["campus_name", "dept_code"], "departments", ignore=True + ("campus_name", "dept_code"), "departments", ignore=True ) def test_add_compound_foreign_key_error_if_column_missing(courses_db): with pytest.raises(AlterError): - courses_db["courses"].add_foreign_key(["campus_name", "nope"], "departments") + courses_db["courses"].add_foreign_key(("campus_name", "nope"), "departments") def test_db_add_foreign_keys_compound(courses_db): @@ -306,15 +309,15 @@ def test_db_add_foreign_keys_compound(courses_db): [ ( "courses", - ["campus_name", "dept_code"], + ("campus_name", "dept_code"), "departments", - ["campus_name", "dept_code"], + ("campus_name", "dept_code"), ) ] ) fk = courses_db["courses"].foreign_keys[0] assert fk.is_compound is True - assert fk.columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") def test_index_foreign_keys_compound_creates_composite_index(compound_db): @@ -424,7 +427,7 @@ def test_implicit_primary_key_reference_is_resolved(): fk = db["books"].foreign_keys[0] assert fk.is_compound is False assert fk.other_column == "author_id" - assert fk.other_columns == ["author_id"] + assert fk.other_columns == ("author_id",) def test_implicit_compound_primary_key_reference_is_resolved(): @@ -444,20 +447,20 @@ def test_implicit_compound_primary_key_reference_is_resolved(): """) fk = db["courses"].foreign_keys[0] assert fk.is_compound is True - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.other_columns == ("campus_name", "dept_code") -def test_foreign_key_normalizes_tuple_columns_to_lists(): - # Compound columns passed as tuples are normalized to lists, so they +def test_foreign_key_normalizes_list_columns_to_tuples(): + # Compound columns passed as lists are normalized to tuples, so they # compare equal to introspected ForeignKeys fk = ForeignKey( table="courses", column=None, other_table="departments", other_column=None, - columns=("campus_name", "dept_code"), - other_columns=("campus_name", "dept_code"), + columns=["campus_name", "dept_code"], + other_columns=["campus_name", "dept_code"], is_compound=True, ) - assert fk.columns == ["campus_name", "dept_code"] - assert fk.other_columns == ["campus_name", "dept_code"] + assert fk.columns == ("campus_name", "dept_code") + assert fk.other_columns == ("campus_name", "dept_code")