From 93bc9ce6fd99e8be9af25d53529a5d5852c50447 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Tue, 14 Jul 2026 23:04:04 -0700 Subject: [PATCH] Fix get_all_foreign_keys crash sorting implicit foreign key targets A foreign key that targets a table's implicit primary key has other_column == None. When two keys to the same table were sorted, the (other_table, column, other_column) sort key compared None to a string and raised TypeError. Coerce the string parts to "" so the sort key is totally ordered. --- datasette/utils/__init__.py | 14 ++++++++++++-- tests/test_utils.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 18d3ba52c9..6c9ab086b6 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -692,11 +692,21 @@ def get_all_foreign_keys(conn): # Sort foreign keys for deterministic ordering for table in table_to_foreign_keys: + # other_column is None when a foreign key targets a table's implicit + # primary key, so coerce to "" to keep the sort key totally ordered. table_to_foreign_keys[table]["incoming"].sort( - key=lambda fk: (fk["other_table"], fk["column"], fk["other_column"]) + key=lambda fk: ( + fk["other_table"] or "", + fk["column"] or "", + fk["other_column"] or "", + ) ) table_to_foreign_keys[table]["outgoing"].sort( - key=lambda fk: (fk["other_table"], fk["column"], fk["other_column"]) + key=lambda fk: ( + fk["other_table"] or "", + fk["column"] or "", + fk["other_column"] or "", + ) ) return table_to_foreign_keys diff --git a/tests/test_utils.py b/tests/test_utils.py index a535ca935c..d7680e24ee 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -921,3 +921,28 @@ def test_deep_dict_update(dict1, dict2, expected): assert result == expected # Check that the original dict1 was modified assert dict1 == expected + + +def test_get_all_foreign_keys_mixed_implicit_and_explicit_targets(): + # Two foreign keys from the same table to the same parent, one targeting + # the implicit primary key (other_column is None) and one an explicit + # column. Sorting the collected keys must not crash comparing None to str. + conn = sqlite3.connect(":memory:") + conn.executescript(""" + CREATE TABLE parent (id INTEGER PRIMARY KEY, alt TEXT UNIQUE); + CREATE TABLE child ( + p_id1 INTEGER, + p_id2 INTEGER, + FOREIGN KEY(p_id1) REFERENCES parent, + FOREIGN KEY(p_id2) REFERENCES parent(alt) + ); + """) + result = utils.get_all_foreign_keys(conn) + assert result["parent"]["incoming"] == [ + {"other_table": "child", "column": None, "other_column": "p_id1"}, + {"other_table": "child", "column": "alt", "other_column": "p_id2"}, + ] + assert result["child"]["outgoing"] == [ + {"other_table": "parent", "column": "p_id1", "other_column": None}, + {"other_table": "parent", "column": "p_id2", "other_column": "alt"}, + ]