From 7a602140df3646820adc45963daf7fc5dcd2a009 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 15 Jul 2025 10:22:56 -0700 Subject: [PATCH] catalog_views table, closes #2495 Refs https://github.com/datasette/datasette-queries/issues/1#issuecomment-3074491003 --- datasette/utils/internal_db.py | 28 ++++++++++++++++++++++++++++ docs/internals.rst | 10 +++++++++- tests/test_internal_db.py | 9 +++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/datasette/utils/internal_db.py b/datasette/utils/internal_db.py index 31d4cbd6..a3afbab2 100644 --- a/datasette/utils/internal_db.py +++ b/datasette/utils/internal_db.py @@ -19,6 +19,14 @@ async def init_internal_db(db): PRIMARY KEY (database_name, table_name), FOREIGN KEY (database_name) REFERENCES catalog_databases(database_name) ); + CREATE TABLE IF NOT EXISTS catalog_views ( + database_name TEXT, + view_name TEXT, + rootpage INTEGER, + sql TEXT, + PRIMARY KEY (database_name, view_name), + FOREIGN KEY (database_name) REFERENCES catalog_databases(database_name) + ); CREATE TABLE IF NOT EXISTS catalog_columns ( database_name TEXT, table_name TEXT, @@ -111,6 +119,9 @@ async def populate_schema_tables(internal_db, db): conn.execute( "DELETE FROM catalog_tables WHERE database_name = ?", [database_name] ) + conn.execute( + "DELETE FROM catalog_views WHERE database_name = ?", [database_name] + ) conn.execute( "DELETE FROM catalog_columns WHERE database_name = ?", [database_name] ) @@ -125,13 +136,21 @@ async def populate_schema_tables(internal_db, db): await internal_db.execute_write_fn(delete_everything) tables = (await db.execute("select * from sqlite_master WHERE type = 'table'")).rows + views = (await db.execute("select * from sqlite_master WHERE type = 'view'")).rows def collect_info(conn): tables_to_insert = [] + views_to_insert = [] columns_to_insert = [] foreign_keys_to_insert = [] indexes_to_insert = [] + for view in views: + view_name = view["name"] + views_to_insert.append( + (database_name, view_name, view["rootpage"], view["sql"]) + ) + for table in tables: table_name = table["name"] tables_to_insert.append( @@ -165,6 +184,7 @@ async def populate_schema_tables(internal_db, db): ) return ( tables_to_insert, + views_to_insert, columns_to_insert, foreign_keys_to_insert, indexes_to_insert, @@ -172,6 +192,7 @@ async def populate_schema_tables(internal_db, db): ( tables_to_insert, + views_to_insert, columns_to_insert, foreign_keys_to_insert, indexes_to_insert, @@ -184,6 +205,13 @@ async def populate_schema_tables(internal_db, db): """, tables_to_insert, ) + await internal_db.execute_write_many( + """ + INSERT INTO catalog_views (database_name, view_name, rootpage, sql) + values (?, ?, ?, ?) + """, + views_to_insert, + ) await internal_db.execute_write_many( """ INSERT INTO catalog_columns ( diff --git a/docs/internals.rst b/docs/internals.rst index 9b4a2388..8575ac14 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1378,7 +1378,7 @@ Datasette's internal database Datasette maintains an "internal" SQLite database used for configuration, caching, and storage. Plugins can store configuration, settings, and other data inside this database. By default, Datasette will use a temporary in-memory SQLite database as the internal database, which is created at startup and destroyed at shutdown. Users of Datasette can optionally pass in a ``--internal`` flag to specify the path to a SQLite database to use as the internal database, which will persist internal data across Datasette instances. -Datasette maintains tables called ``catalog_databases``, ``catalog_tables``, ``catalog_columns``, ``catalog_indexes``, ``catalog_foreign_keys`` with details of the attached databases and their schemas. These tables should not be considered a stable API - they may change between Datasette releases. +Datasette maintains tables called ``catalog_databases``, ``catalog_tables``, ``catalog_views``, ``catalog_columns``, ``catalog_indexes``, ``catalog_foreign_keys`` with details of the attached databases and their schemas. These tables should not be considered a stable API - they may change between Datasette releases. Metadata is stored in tables ``metadata_instance``, ``metadata_databases``, ``metadata_resources`` and ``metadata_columns``. Plugins can interact with these tables via the :ref:`get_*_metadata() and set_*_metadata() methods `. @@ -1421,6 +1421,14 @@ The internal database schema is as follows: PRIMARY KEY (database_name, table_name), FOREIGN KEY (database_name) REFERENCES catalog_databases(database_name) ); + CREATE TABLE catalog_views ( + database_name TEXT, + view_name TEXT, + rootpage INTEGER, + sql TEXT, + PRIMARY KEY (database_name, view_name), + FOREIGN KEY (database_name) REFERENCES catalog_databases(database_name) + ); CREATE TABLE catalog_columns ( database_name TEXT, table_name TEXT, diff --git a/tests/test_internal_db.py b/tests/test_internal_db.py index 246d795e..59516225 100644 --- a/tests/test_internal_db.py +++ b/tests/test_internal_db.py @@ -25,6 +25,15 @@ async def test_internal_tables(ds_client): assert set(table.keys()) == {"rootpage", "table_name", "database_name", "sql"} +@pytest.mark.asyncio +async def test_internal_views(ds_client): + internal_db = await ensure_internal(ds_client) + views = await internal_db.execute("select * from catalog_views") + assert len(views) >= 4 + view = views.rows[0] + assert set(view.keys()) == {"rootpage", "view_name", "database_name", "sql"} + + @pytest.mark.asyncio async def test_internal_indexes(ds_client): internal_db = await ensure_internal(ds_client)