diff --git a/datasette/templates/query.html b/datasette/templates/query.html index 1900bd31..785b05af 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -47,14 +47,14 @@ {% if not hide_sql %} {% if editable and allow_execute_sql %}

+ >{% if query and query.sql %}{{ query.sql }}{% elif tables %}select * from {{ tables[0].name|escape_sqlite }}{% endif %}

{% else %}
{% if query %}{{ query.sql }}{% endif %}
{% endif %} {% else %} {% if not canned_query %} {% endif %} {% endif %} diff --git a/datasette/views/database.py b/datasette/views/database.py index aafcf40b..feb38619 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -1776,7 +1776,7 @@ class QueryView(View): named_parameters = [] if canned_query and canned_query.get("params"): named_parameters = canned_query["params"] - if not named_parameters: + if not named_parameters and sql: named_parameters = derive_named_parameters(sql) named_parameter_values = { named_parameter: params.get(named_parameter) or "" @@ -1801,7 +1801,7 @@ class QueryView(View): params_for_query = params - if not canned_query_write: + if sql and not canned_query_write: try: if not canned_query: # For regular queries we only allow SELECT, plus other rules @@ -1845,6 +1845,8 @@ class QueryView(View): # Handle formats from plugins if format_ == "csv": + if not sql: + raise DatasetteError("?sql= is required", status=400) async def fetch_data_for_csv(request, _next=None): results = await db.execute(sql, params, truncate=True) @@ -1978,25 +1980,26 @@ class QueryView(View): # - No magic parameters, so no :_ in the SQL string edit_sql_url = None is_validated_sql = False - try: - validate_sql_select(sql) - is_validated_sql = True - except InvalidSql: - pass - if allow_execute_sql and is_validated_sql and ":_" not in sql: - edit_sql_url = ( - datasette.urls.database(database) - + "/-/query" - + "?" - + urlencode( - { - **{ - "sql": sql, - }, - **named_parameter_values, - } + if sql: + try: + validate_sql_select(sql) + is_validated_sql = True + except InvalidSql: + pass + if allow_execute_sql and is_validated_sql and ":_" not in sql: + edit_sql_url = ( + datasette.urls.database(database) + + "/-/query" + + "?" + + urlencode( + { + **{ + "sql": sql, + }, + **named_parameter_values, + } + ) ) - ) save_query_url = None if ( not canned_query diff --git a/docs/changelog.rst b/docs/changelog.rst index 329b4769..dfb2a736 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,13 @@ Changelog ========= +.. _v1_0_unreleased: + +Unreleased +---------- + +- Fixed a bug where visiting ``//-/query`` without a ``?sql=`` parameter returned a 500 error. (:issue:`2743`) + .. _v1_0_a30: 1.0a30 (2026-05-24) diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index 1dd9ed3e..1d2378b4 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -382,8 +382,8 @@ def view_actions(datasette, database, view, actor): @hookimpl def query_actions(datasette, database, query_name, sql): - # Don't explain an explain - if sql.lower().startswith("explain"): + # Don't explain an explain (or a missing query) + if not sql or sql.lower().startswith("explain"): return return [ { diff --git a/tests/test_html.py b/tests/test_html.py index 8cda6dba..9e460da1 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -239,6 +239,22 @@ def test_query_page_truncates(): ] +@pytest.mark.asyncio +async def test_query_page_with_no_sql(ds_client): + # https://github.com/simonw/datasette/issues/2743 + response = await ds_client.get("/fixtures/-/query") + assert response.status_code == 200 + assert '