Compare commits

...

3 commits

Author SHA1 Message Date
Alex Garcia
999b9f0353 test only on SQLite 3.37 and above 2024-08-15 09:37:54 -07:00
Alex Garcia
86c5203451 fmt 2024-08-14 15:45:22 -07:00
Alex Garcia
751abbcc57 don't hide virtual table, hide shadow tables. 2024-08-14 15:39:49 -07:00
4 changed files with 120 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,56 @@ 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 final as (
select name
from sqlite_master
WHERE name in ('sqlite_stat1', 'sqlite_stat2', 'sqlite_stat3', 'sqlite_stat4')
OR substr(name, 1, 1) == '_'
),
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 +538,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

@ -39,13 +39,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,48 @@ 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",
]