mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
parent
59994e18e4
commit
bda69ff1c9
2 changed files with 83 additions and 25 deletions
|
|
@ -930,20 +930,18 @@ class TablesView(BaseView):
|
||||||
# Get search query parameter
|
# Get search query parameter
|
||||||
q = request.args.get("q", "").strip()
|
q = request.args.get("q", "").strip()
|
||||||
|
|
||||||
# Only return matches if there's a non-empty search query
|
|
||||||
if not q:
|
|
||||||
return Response.json({"matches": []})
|
|
||||||
|
|
||||||
# Build SQL LIKE pattern from search terms
|
|
||||||
# Split search terms by whitespace and build pattern: %term1%term2%term3%
|
|
||||||
terms = q.split()
|
|
||||||
pattern = "%" + "%".join(terms) + "%"
|
|
||||||
|
|
||||||
# Get SQL for allowed resources using the permission system
|
# Get SQL for allowed resources using the permission system
|
||||||
permission_sql, params = await self.ds.allowed_resources_sql(
|
permission_sql, params = await self.ds.allowed_resources_sql(
|
||||||
action="view-table", actor=request.actor
|
action="view-table", actor=request.actor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build query based on whether we have a search query
|
||||||
|
if q:
|
||||||
|
# Build SQL LIKE pattern from search terms
|
||||||
|
# Split search terms by whitespace and build pattern: %term1%term2%term3%
|
||||||
|
terms = q.split()
|
||||||
|
pattern = "%" + "%".join(terms) + "%"
|
||||||
|
|
||||||
# Build query with CTE to filter by search pattern
|
# Build query with CTE to filter by search pattern
|
||||||
sql = f"""
|
sql = f"""
|
||||||
WITH allowed_tables AS (
|
WITH allowed_tables AS (
|
||||||
|
|
@ -954,20 +952,36 @@ class TablesView(BaseView):
|
||||||
WHERE child LIKE :pattern COLLATE NOCASE
|
WHERE child LIKE :pattern COLLATE NOCASE
|
||||||
ORDER BY length(child), child
|
ORDER BY length(child), child
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Merge params from permission SQL with our pattern param
|
|
||||||
all_params = {**params, "pattern": pattern}
|
all_params = {**params, "pattern": pattern}
|
||||||
|
else:
|
||||||
|
# No search query - return all tables, ordered by name
|
||||||
|
# Fetch 101 to detect if we need to truncate
|
||||||
|
sql = f"""
|
||||||
|
WITH allowed_tables AS (
|
||||||
|
{permission_sql}
|
||||||
|
)
|
||||||
|
SELECT parent, child
|
||||||
|
FROM allowed_tables
|
||||||
|
ORDER BY parent, child
|
||||||
|
LIMIT 101
|
||||||
|
"""
|
||||||
|
all_params = params
|
||||||
|
|
||||||
# Execute against internal database
|
# Execute against internal database
|
||||||
result = await self.ds.get_internal_database().execute(sql, all_params)
|
result = await self.ds.get_internal_database().execute(sql, all_params)
|
||||||
|
|
||||||
# Build response
|
# Build response with truncation
|
||||||
|
rows = list(result.rows)
|
||||||
|
truncated = len(rows) > 100
|
||||||
|
if truncated:
|
||||||
|
rows = rows[:100]
|
||||||
|
|
||||||
matches = [
|
matches = [
|
||||||
{
|
{
|
||||||
"name": f"{row['parent']}: {row['child']}",
|
"name": f"{row['parent']}: {row['child']}",
|
||||||
"url": self.ds.urls.table(row["parent"], row["child"]),
|
"url": self.ds.urls.table(row["parent"], row["child"]),
|
||||||
}
|
}
|
||||||
for row in result.rows
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
return Response.json({"matches": matches})
|
return Response.json({"matches": matches, "truncated": truncated})
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,50 @@ async def test_tables_endpoint_empty_result(test_ds):
|
||||||
pm.unregister(plugin, name="test_plugin")
|
pm.unregister(plugin, name="test_plugin")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tables_endpoint_no_query_returns_all():
|
||||||
|
"""Test /-/tables without query parameter returns all tables"""
|
||||||
|
ds = Datasette()
|
||||||
|
await ds.invoke_startup()
|
||||||
|
|
||||||
|
# Add database with a few tables
|
||||||
|
db = ds.add_memory_database("test_db")
|
||||||
|
await db.execute_write("CREATE TABLE users (id INTEGER)")
|
||||||
|
await db.execute_write("CREATE TABLE posts (id INTEGER)")
|
||||||
|
await db.execute_write("CREATE TABLE comments (id INTEGER)")
|
||||||
|
await ds._refresh_schemas()
|
||||||
|
|
||||||
|
# Get all tables without query
|
||||||
|
all_tables = await ds.allowed_resources("view-table", None)
|
||||||
|
|
||||||
|
# Should return all tables with truncated: false
|
||||||
|
assert len(all_tables) >= 3
|
||||||
|
table_names = {f"{t.parent}/{t.child}" for t in all_tables}
|
||||||
|
assert "test_db/users" in table_names
|
||||||
|
assert "test_db/posts" in table_names
|
||||||
|
assert "test_db/comments" in table_names
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tables_endpoint_truncation():
|
||||||
|
"""Test /-/tables truncates at 100 tables and sets truncated flag"""
|
||||||
|
ds = Datasette()
|
||||||
|
await ds.invoke_startup()
|
||||||
|
|
||||||
|
# Create a database with 105 tables
|
||||||
|
db = ds.add_memory_database("big_db")
|
||||||
|
for i in range(105):
|
||||||
|
await db.execute_write(f"CREATE TABLE table_{i:03d} (id INTEGER)")
|
||||||
|
await ds._refresh_schemas()
|
||||||
|
|
||||||
|
# Get all tables - should be truncated
|
||||||
|
all_tables = await ds.allowed_resources("view-table", None)
|
||||||
|
big_db_tables = [t for t in all_tables if t.parent == "big_db"]
|
||||||
|
|
||||||
|
# Should have exactly 105 tables in the database
|
||||||
|
assert len(big_db_tables) == 105
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_tables_endpoint_search_single_term():
|
async def test_tables_endpoint_search_single_term():
|
||||||
"""Test /-/tables?q=user to filter tables matching 'user'"""
|
"""Test /-/tables?q=user to filter tables matching 'user'"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue