From 21c156dfb1ca76d8e8c2e04c044766a155e22edc Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 18 Jun 2026 08:13:28 -0700 Subject: [PATCH] Expose foreign key targets to create table UI - Add foreignKeyTargetsPath to create table page data - Filter hidden tables from database-level foreign key target results - Update JSON API docs and tests for filtered targets --- datasette/views/table_create_alter.py | 12 +++++++++++- docs/json_api.rst | 2 +- tests/test_api_write.py | 8 ++++++++ tests/test_table_html.py | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/datasette/views/table_create_alter.py b/datasette/views/table_create_alter.py index ce43199c..f7775479 100644 --- a/datasette/views/table_create_alter.py +++ b/datasette/views/table_create_alter.py @@ -262,6 +262,9 @@ async def _create_table_ui_context( return None data = { "path": "{}/-/create".format(datasette.urls.database(database_name)), + "foreignKeyTargetsPath": "{}/-/foreign-key-targets".format( + datasette.urls.database(database_name) + ), "databaseName": database_name, "columnTypes": CREATE_TABLE_COLUMN_TYPES, } @@ -836,7 +839,14 @@ class DatabaseForeignKeyTargetsView(BaseView): ): return _error(["Permission denied: need create-table"], 403) - targets = (await db.execute(FOREIGN_KEY_TARGETS_SQL)).dicts() + hidden_tables = await db.execute_fn( + lambda conn: set(sqlite_hidden_table_names(conn)) + ) + targets = [ + target + for target in (await db.execute(FOREIGN_KEY_TARGETS_SQL)).dicts() + if target["fk_table"] not in hidden_tables + ] return Response.json( { "ok": True, diff --git a/docs/json_api.rst b/docs/json_api.rst index dee98ef2..1db46dd2 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -2108,7 +2108,7 @@ The ``//-/foreign-key-targets`` endpoint returns the list of tables in GET //-/foreign-key-targets -The response includes only tables with exactly one primary key column. Tables with compound primary keys and tables with no explicit primary key are omitted. +The response includes only tables with exactly one primary key column. Hidden tables, tables with compound primary keys and tables with no explicit primary key are omitted. Each target includes the normalized SQLite type affinity for the primary key column in ``type``. The type is calculated using SQLite's documented affinity rules: ``INT`` maps to ``integer``; ``CHAR``, ``CLOB`` or ``TEXT`` maps to ``text``; ``BLOB`` or no type maps to ``blob``; ``REAL`` and floating-point declared types map to ``real``; everything else maps to ``numeric``. diff --git a/tests/test_api_write.py b/tests/test_api_write.py index 18ffe43d..1d16ad26 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -1161,6 +1161,10 @@ async def test_foreign_key_targets(ds_write): "create table compound (a integer, b integer, primary key (a, b))" ) await db.execute_write("create table no_pk (name text)") + try: + await db.execute_write("create virtual table search_docs using fts5(body)") + except Exception: + pass response = await ds_write.client.get( "/data/-/foreign-key-targets", @@ -1203,6 +1207,10 @@ async def test_foreign_key_targets(ds_write): }, ], } + assert not any( + target["fk_table"].startswith("search_docs_") + for target in response.json()["targets"] + ) @pytest.mark.asyncio diff --git a/tests/test_table_html.py b/tests/test_table_html.py index 374cc08d..fa01c8ec 100644 --- a/tests/test_table_html.py +++ b/tests/test_table_html.py @@ -994,6 +994,7 @@ async def test_database_create_table_action_button_and_data(): assert database_data_from_soup(soup) == { "createTable": { "path": "/data/-/create", + "foreignKeyTargetsPath": "/data/-/foreign-key-targets", "databaseName": "data", "columnTypes": ["text", "integer", "float", "blob"], },