Migrate /database view to use bulk allowed_resources()

Replace one-by-one permission checks with bulk allowed_resources() call:
- DatabaseView and QueryView now fetch all allowed tables once
- Filter views and tables using pre-fetched allowed_table_set
- Update TableResource.resources_sql() to include views from catalog_views

This improves performance by reducing permission checks from O(n) to O(1) per
table/view, where n is the number of tables in the database.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2025-10-24 00:28:16 -07:00
commit 2620938661
2 changed files with 40 additions and 15 deletions

View file

@ -48,6 +48,9 @@ class TableResource(Resource):
return """
SELECT database_name AS parent, table_name AS child
FROM catalog_tables
UNION ALL
SELECT database_name AS parent, view_name AS child
FROM catalog_views
"""

View file

@ -71,17 +71,26 @@ class DatabaseView(View):
metadata = await datasette.get_database_metadata(database)
# Get all tables/views this actor can see in bulk
from datasette.resources import TableResource
allowed_tables = await datasette.allowed_resources("view-table", request.actor)
allowed_table_set = {
(r.parent, r.child) for r in allowed_tables if r.parent == database
}
sql_views = []
for view_name in await db.view_names():
view_visible, view_private = await datasette.check_visibility(
request.actor,
permissions=[
("view-table", (database, view_name)),
("view-database", database),
"view-instance",
],
)
if view_visible:
if (database, view_name) in allowed_table_set:
# Check if it's private (requires elevated permissions)
_, view_private = await datasette.check_visibility(
request.actor,
permissions=[
("view-table", (database, view_name)),
("view-database", database),
"view-instance",
],
)
sql_views.append(
{
"name": view_name,
@ -89,7 +98,7 @@ class DatabaseView(View):
}
)
tables = await get_tables(datasette, request, db)
tables = await get_tables(datasette, request, db, allowed_table_set)
canned_queries = []
for query in (
await datasette.get_canned_queries(database, request.actor)
@ -332,7 +341,7 @@ class QueryContext(Context):
)
async def get_tables(datasette, request, db):
async def get_tables(datasette, request, db, allowed_table_set):
tables = []
database = db.name
table_counts = await db.table_counts(100)
@ -340,7 +349,11 @@ async def get_tables(datasette, request, db):
all_foreign_keys = await db.get_all_foreign_keys()
for table in table_counts:
table_visible, table_private = await datasette.check_visibility(
if (database, table) not in allowed_table_set:
continue
# Check if it's private (requires elevated permissions)
_, table_private = await datasette.check_visibility(
request.actor,
permissions=[
("view-table", (database, table)),
@ -348,8 +361,7 @@ async def get_tables(datasette, request, db):
"view-instance",
],
)
if not table_visible:
continue
table_columns = await db.table_columns(table)
tables.append(
{
@ -509,6 +521,14 @@ class QueryView(View):
db = await datasette.resolve_database(request)
database = db.name
# Get all tables/views this actor can see in bulk
from datasette.resources import TableResource
allowed_tables = await datasette.allowed_resources("view-table", request.actor)
allowed_table_set = {
(r.parent, r.child) for r in allowed_tables if r.parent == database
}
# Are we a canned query?
canned_query = None
canned_query_write = False
@ -808,7 +828,9 @@ class QueryView(View):
show_hide_text=show_hide_text,
editable=not canned_query,
allow_execute_sql=allow_execute_sql,
tables=await get_tables(datasette, request, db),
tables=await get_tables(
datasette, request, db, allowed_table_set
),
named_parameter_values=named_parameter_values,
edit_sql_url=edit_sql_url,
display_rows=await display_rows(