Hide shadow tables, don't hide virtual tables

Closes #2296
This commit is contained in:
Alex Garcia 2024-08-15 13:19:22 -07:00 committed by GitHub
commit 6d91d082e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 161 additions and 52 deletions

View file

@ -20,6 +20,7 @@ from .utils import (
table_columns, table_columns,
table_column_details, table_column_details,
) )
from .utils.sqlite import sqlite_version
from .inspect import inspect_hash from .inspect import inspect_hash
connections = threading.local() connections = threading.local()
@ -459,22 +460,95 @@ class Database:
) )
async def hidden_table_names(self): async def hidden_table_names(self):
# Mark tables 'hidden' if they relate to FTS virtual tables hidden_tables = []
hidden_tables = [ # Add any tables marked as hidden in config
r[0] db_config = self.ds.config.get("databases", {}).get(self.name, {})
for r in ( if "tables" in db_config:
await self.execute( hidden_tables += [
t for t in db_config["tables"] if db_config["tables"][t].get("hidden")
]
if sqlite_version()[1] >= 37:
hidden_tables += [
x[0]
for x in await self.execute(
"""
with shadow_tables as (
select name
from pragma_table_list
where [type] = 'shadow'
order by name
),
core_tables as (
select name
from sqlite_master
WHERE name in ('sqlite_stat1', 'sqlite_stat2', 'sqlite_stat3', 'sqlite_stat4')
OR substr(name, 1, 1) == '_'
),
combined as (
select name from shadow_tables
union all
select name from core_tables
)
select name from combined order by 1
""" """
select name from sqlite_master
where rootpage = 0
and (
sql like '%VIRTUAL TABLE%USING FTS%'
) or name in ('sqlite_stat1', 'sqlite_stat2', 'sqlite_stat3', 'sqlite_stat4')
or name like '\\_%' escape '\\'
"""
) )
).rows ]
] else:
hidden_tables += [
x[0]
for x in await self.execute(
"""
WITH base AS (
SELECT name
FROM sqlite_master
WHERE name IN ('sqlite_stat1', 'sqlite_stat2', 'sqlite_stat3', 'sqlite_stat4')
OR substr(name, 1, 1) == '_'
),
fts_suffixes AS (
SELECT column1 AS suffix
FROM (VALUES ('_data'), ('_idx'), ('_docsize'), ('_content'), ('_config'))
),
fts5_names AS (
SELECT name
FROM sqlite_master
WHERE sql LIKE '%VIRTUAL TABLE%USING FTS%'
),
fts5_shadow_tables AS (
SELECT
printf('%s%s', fts5_names.name, fts_suffixes.suffix) AS name
FROM fts5_names
JOIN fts_suffixes
),
fts3_suffixes AS (
SELECT column1 AS suffix
FROM (VALUES ('_content'), ('_segdir'), ('_segments'), ('_stat'), ('_docsize'))
),
fts3_names AS (
SELECT name
FROM sqlite_master
WHERE sql LIKE '%VIRTUAL TABLE%USING FTS3%'
OR sql LIKE '%VIRTUAL TABLE%USING FTS4%'
),
fts3_shadow_tables AS (
SELECT
printf('%s%s', fts3_names.name, fts3_suffixes.suffix) AS name
FROM fts3_names
JOIN fts3_suffixes
),
final AS (
SELECT name FROM base
UNION ALL
SELECT name FROM fts5_shadow_tables
UNION ALL
SELECT name FROM fts3_shadow_tables
)
SELECT name FROM final ORDER BY 1
"""
)
]
has_spatialite = await self.execute_fn(detect_spatialite) has_spatialite = await self.execute_fn(detect_spatialite)
if has_spatialite: if has_spatialite:
# Also hide Spatialite internal tables # Also hide Spatialite internal tables
@ -503,19 +577,6 @@ class Database:
) )
).rows ).rows
] ]
# Add any tables marked as hidden in config
db_config = self.ds.config.get("databases", {}).get(self.name, {})
if "tables" in db_config:
hidden_tables += [
t for t in db_config["tables"] if db_config["tables"][t].get("hidden")
]
# Also mark as hidden any tables which start with the name of a hidden table
# e.g. "searchable_fts" implies "searchable_fts_content" should be hidden
for table_name in await self.table_names():
for hidden_table in hidden_tables[:]:
if table_name.startswith(hidden_table):
hidden_tables.append(table_name)
continue
return hidden_tables return hidden_tables

View file

@ -389,6 +389,29 @@ async def test_database_page(ds_client):
}, },
"private": False, "private": False,
}, },
{
"name": "searchable_fts",
"columns": [
"text1",
"text2",
"name with . and spaces",
]
+ (
[
"searchable_fts",
"docid",
"__langid",
]
if supports_table_xinfo()
else []
),
"primary_keys": [],
"count": 2,
"hidden": False,
"fts_table": "searchable_fts",
"foreign_keys": {"incoming": [], "outgoing": []},
"private": False,
},
{ {
"name": "searchable_tags", "name": "searchable_tags",
"columns": ["searchable_id", "tag"], "columns": ["searchable_id", "tag"],
@ -525,29 +548,6 @@ async def test_database_page(ds_client):
"foreign_keys": {"incoming": [], "outgoing": []}, "foreign_keys": {"incoming": [], "outgoing": []},
"private": False, "private": False,
}, },
{
"name": "searchable_fts",
"columns": [
"text1",
"text2",
"name with . and spaces",
]
+ (
[
"searchable_fts",
"docid",
"__langid",
]
if supports_table_xinfo()
else []
),
"primary_keys": [],
"count": 2,
"hidden": True,
"fts_table": "searchable_fts",
"foreign_keys": {"incoming": [], "outgoing": []},
"private": False,
},
{ {
"name": "searchable_fts_docsize", "name": "searchable_fts_docsize",
"columns": ["docid", "size"], "columns": ["docid", "size"],

View file

@ -41,13 +41,14 @@ def test_homepage(app_client_two_attached_databases):
assert "extra database" == h2.text.strip() assert "extra database" == h2.text.strip()
counts_p, links_p = h2.find_all_next("p")[:2] counts_p, links_p = h2.find_all_next("p")[:2]
assert ( assert (
"2 rows in 1 table, 5 rows in 4 hidden tables, 1 view" == counts_p.text.strip() "4 rows in 2 tables, 3 rows in 3 hidden tables, 1 view" == counts_p.text.strip()
) )
# We should only show visible, not hidden tables here: # We should only show visible, not hidden tables here:
table_links = [ table_links = [
{"href": a["href"], "text": a.text.strip()} for a in links_p.findAll("a") {"href": a["href"], "text": a.text.strip()} for a in links_p.findAll("a")
] ]
assert [ assert [
{"href": r"/extra+database/searchable_fts", "text": "searchable_fts"},
{"href": r"/extra+database/searchable", "text": "searchable"}, {"href": r"/extra+database/searchable", "text": "searchable"},
{"href": r"/extra+database/searchable_view", "text": "searchable_view"}, {"href": r"/extra+database/searchable_view", "text": "searchable_view"},
] == table_links ] == table_links

View file

@ -4,7 +4,7 @@ Tests for the datasette.database.Database class
from datasette.app import Datasette from datasette.app import Datasette
from datasette.database import Database, Results, MultipleValues from datasette.database import Database, Results, MultipleValues
from datasette.utils.sqlite import sqlite3 from datasette.utils.sqlite import sqlite3, sqlite_version
from datasette.utils import Column from datasette.utils import Column
from .fixtures import app_client, app_client_two_attached_databases_crossdb_enabled from .fixtures import app_client, app_client_two_attached_databases_crossdb_enabled
import pytest import pytest
@ -664,3 +664,50 @@ async def test_in_memory_databases_forbid_writes(app_client):
# Using db.execute_write() should work: # Using db.execute_write() should work:
await db.execute_write("create table foo (t text)") await db.execute_write("create table foo (t text)")
assert await db.table_names() == ["foo"] assert await db.table_names() == ["foo"]
def pragma_table_list_supported():
return sqlite_version()[1] >= 37
@pytest.mark.asyncio
@pytest.mark.skipif(
not pragma_table_list_supported(), reason="Requires PRAGMA table_list support"
)
async def test_hidden_tables(app_client):
ds = app_client.ds
db = ds.add_database(Database(ds, is_memory=True, is_mutable=True))
assert await db.hidden_table_names() == []
await db.execute("create virtual table f using fts5(a)")
assert await db.hidden_table_names() == [
"f_config",
"f_content",
"f_data",
"f_docsize",
"f_idx",
]
await db.execute("create virtual table r using rtree(id, amin, amax)")
assert await db.hidden_table_names() == [
"f_config",
"f_content",
"f_data",
"f_docsize",
"f_idx",
"r_node",
"r_parent",
"r_rowid",
]
await db.execute("create table _hideme(_)")
assert await db.hidden_table_names() == [
"_hideme",
"f_config",
"f_content",
"f_data",
"f_docsize",
"f_idx",
"r_node",
"r_parent",
"r_rowid",
]