diff --git a/datasette/views/special.py b/datasette/views/special.py index 631b554c..ca155a04 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -930,44 +930,58 @@ class TablesView(BaseView): # Get search query parameter 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 permission_sql, params = await self.ds.allowed_resources_sql( action="view-table", actor=request.actor ) - # Build query with CTE to filter by search pattern - sql = f""" - WITH allowed_tables AS ( - {permission_sql} - ) - SELECT parent, child - FROM allowed_tables - WHERE child LIKE :pattern COLLATE NOCASE - ORDER BY length(child), child - """ + # 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) + "%" - # Merge params from permission SQL with our pattern param - all_params = {**params, "pattern": pattern} + # Build query with CTE to filter by search pattern + sql = f""" + WITH allowed_tables AS ( + {permission_sql} + ) + SELECT parent, child + FROM allowed_tables + WHERE child LIKE :pattern COLLATE NOCASE + ORDER BY length(child), child + """ + 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 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 = [ { "name": f"{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}) diff --git a/tests/test_tables_endpoint.py b/tests/test_tables_endpoint.py index 6b29a2f7..e6b821e5 100644 --- a/tests/test_tables_endpoint.py +++ b/tests/test_tables_endpoint.py @@ -313,6 +313,50 @@ async def test_tables_endpoint_empty_result(test_ds): 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 async def test_tables_endpoint_search_single_term(): """Test /-/tables?q=user to filter tables matching 'user'"""