diff --git a/datasette/templates/_query_results.html b/datasette/templates/_query_results.html new file mode 100644 index 00000000..5e1e2f72 --- /dev/null +++ b/datasette/templates/_query_results.html @@ -0,0 +1,20 @@ +{% if display_rows %} +
+ + + {% for column in columns %}{% endfor %} + + + + {% for row in display_rows %} + + {% for column, td in zip(columns, row) %} + + {% endfor %} + + {% endfor %} + +
{{ column }}
{{ td }}
+{% elif show_zero_results %} +

0 results

+{% endif %} diff --git a/datasette/templates/execute_write.html b/datasette/templates/execute_write.html index 394261de..a93de3a6 100644 --- a/datasette/templates/execute_write.html +++ b/datasette/templates/execute_write.html @@ -81,6 +81,17 @@ form.sql.core input[data-execute-write-submit]:disabled {

{{ execution_message }}{% for link in execution_links %} {{ link.label }}{% endfor %}

{% endif %} +{% if execute_write_returns_rows %} +

Returned rows

+ {% if execute_write_truncated %} +

Only the first {{ "{:,}".format(execute_write_display_rows|length) }} returned rows are shown.

+ {% endif %} + {% set columns = execute_write_columns %} + {% set display_rows = execute_write_display_rows %} + {% set show_zero_results = true %} + {% include "_query_results.html" %} +{% endif %} +
{% if write_template_tables %}
diff --git a/datasette/templates/query.html b/datasette/templates/query.html index 168a636b..8dd1037f 100644 --- a/datasette/templates/query.html +++ b/datasette/templates/query.html @@ -73,27 +73,9 @@ {% if display_rows %} -
- - - {% for column in columns %}{% endfor %} - - - - {% for row in display_rows %} - - {% for column, td in zip(columns, row) %} - - {% endfor %} - - {% endfor %} - -
{{ column }}
{{ td }}
-{% else %} - {% if not stored_query_write and not error %} -

0 results

- {% endif %} {% endif %} +{% set show_zero_results = not stored_query_write and not error %} +{% include "_query_results.html" %} {% include "_codemirror_foot.html" %} {% include "_sql_parameter_scripts.html" %} diff --git a/datasette/views/execute_write.py b/datasette/views/execute_write.py index 5d3912dd..c5d55b80 100644 --- a/datasette/views/execute_write.py +++ b/datasette/views/execute_write.py @@ -6,6 +6,7 @@ from datasette.utils import sqlite3 from datasette.utils.asgi import Response from .base import BaseView, _error +from .database import display_rows as display_query_rows from .query_helpers import ( QueryValidationError, _analysis_is_write, @@ -221,10 +222,16 @@ class ExecuteWriteView(BaseView): execution_message=None, execution_links=None, execution_ok=None, + execute_write_returns_rows=False, + execute_write_columns=None, + execute_write_display_rows=None, + execute_write_truncated=False, status=200, ): parameter_values = parameter_values or {} execution_links = execution_links or [] + execute_write_columns = execute_write_columns or [] + execute_write_display_rows = execute_write_display_rows or [] parameter_names = [] analysis_rows = [] table_columns = await _table_columns(self.ds, db.name) @@ -284,6 +291,10 @@ class ExecuteWriteView(BaseView): "execution_message": execution_message, "execution_links": execution_links, "execution_ok": execution_ok, + "execute_write_returns_rows": execute_write_returns_rows, + "execute_write_columns": execute_write_columns, + "execute_write_display_rows": execute_write_display_rows, + "execute_write_truncated": execute_write_truncated, "execute_disabled": bool(execute_disabled_reason), "execute_disabled_reason": execute_disabled_reason, "table_columns": table_columns, @@ -358,8 +369,6 @@ class ExecuteWriteView(BaseView): wants_json = _wants_json(request, is_json, data) try: execute_write_kwargs = {"request": request} - if wants_json: - execute_write_kwargs["returning_limit"] = self.ds.max_returned_rows cursor = await db.execute_write(sql, params, **execute_write_kwargs) except sqlite3.DatabaseError as ex: message = str(ex) @@ -402,6 +411,20 @@ class ExecuteWriteView(BaseView): if inserted_row_url else [] ) + execute_write_returns_rows = cursor.description is not None + execute_write_columns = [] + execute_write_display_rows = [] + if execute_write_returns_rows: + execute_write_columns = [ + description[0] for description in cursor.description + ] + execute_write_display_rows = await display_query_rows( + self.ds, + db.name, + request, + cursor.fetchall(), + execute_write_columns, + ) return await self._render_form( request, db, @@ -411,6 +434,10 @@ class ExecuteWriteView(BaseView): execution_message=message, execution_links=execution_links, execution_ok=True, + execute_write_returns_rows=execute_write_returns_rows, + execute_write_columns=execute_write_columns, + execute_write_display_rows=execute_write_display_rows, + execute_write_truncated=cursor.truncated, ) diff --git a/docs/json_api.rst b/docs/json_api.rst index a8384d0b..65031bf4 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -584,7 +584,7 @@ For most write statements ``"rows"`` will be an empty list and ``"truncated"`` will be ``false``. If the SQL uses SQLite's ``RETURNING`` clause, ``"rows"`` will contain returned rows using the same default representation as table and query JSON responses. ``"truncated"`` indicates if more rows were returned than -the configured :ref:`setting_max_returned_rows` limit: +the execute-write returning row limit, which defaults to 10: .. code-block:: json diff --git a/tests/test_queries.py b/tests/test_queries.py index cf7e727c..7aa9883b 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -1922,18 +1922,14 @@ async def test_execute_write_json_includes_returning_rows(): @pytest.mark.asyncio async def test_execute_write_json_returning_rows_can_be_truncated(): - ds = Datasette( - memory=True, - default_deny=True, - settings={"max_returned_rows": 2}, - ) + ds = Datasette(memory=True, default_deny=True) ds.root_enabled = True db = ds.add_memory_database("execute_write_returning_json_truncated", name="data") await db.execute_write("create table dogs (id integer primary key, name text)") - await db.execute_write( - "insert into dogs (name) values " - "('Cleo'), ('Pancakes'), ('Nixie'), ('Marnie')" - ) + for index in range(1, 12): + await db.execute_write( + "insert into dogs (name) values (?)", ["Dog {}".format(index)] + ) await ds.invoke_startup() response = await ds.client.post( @@ -1945,16 +1941,81 @@ async def test_execute_write_json_returning_rows_can_be_truncated(): assert response.status_code == 200 data = response.json() assert data["ok"] is True - assert data["message"] == "Query executed" - assert data["rowcount"] == -1 + assert data["message"] == "Query executed, 11 rows affected" + assert data["rowcount"] == 11 assert data["rows"] == [ - {"id": 1, "name": "Cleo!"}, - {"id": 2, "name": "Pancakes!"}, + {"id": index, "name": "Dog {}!".format(index)} for index in range(1, 11) ] assert data["truncated"] is True assert (await db.execute("select count(*) from dogs where name like '%!'")).first()[ 0 - ] == 4 + ] == 11 + + +@pytest.mark.asyncio +async def test_execute_write_html_displays_returning_rows(): + ds = Datasette(memory=True, default_deny=True) + ds.root_enabled = True + db = ds.add_memory_database("execute_write_returning_html", name="data") + await db.execute_write("create table dogs (id integer primary key, name text)") + await ds.invoke_startup() + + response = await ds.client.post( + "/data/-/execute-write", + actor={"id": "root"}, + data={ + "sql": "insert into dogs (name) values (:name) returning id, name", + "name": "Cleo", + }, + ) + non_returning_response = await ds.client.post( + "/data/-/execute-write", + actor={"id": "root"}, + data={"sql": "insert into dogs (name) values ('Pancakes')"}, + ) + + assert response.status_code == 200 + assert "Query executed, 1 row affected" in response.text + assert "

Returned rows

" in response.text + assert '' in response.text + assert '' in response.text + assert '' in response.text + assert '' in response.text + assert '' in response.text + + assert non_returning_response.status_code == 200 + assert "Query executed, 1 row affected" in non_returning_response.text + assert "

Returned rows

" not in non_returning_response.text + assert '

0 results

' not in non_returning_response.text + + +@pytest.mark.asyncio +async def test_execute_write_html_returning_rows_can_be_truncated(): + ds = Datasette(memory=True, default_deny=True) + ds.root_enabled = True + db = ds.add_memory_database("execute_write_returning_html_truncated", name="data") + await db.execute_write("create table dogs (id integer primary key, name text)") + for index in range(1, 12): + await db.execute_write( + "insert into dogs (name) values (?)", ["Dog {}".format(index)] + ) + await ds.invoke_startup() + + response = await ds.client.post( + "/data/-/execute-write", + actor={"id": "root"}, + data={"sql": "update dogs set name = name || '!' returning id, name"}, + ) + + assert response.status_code == 200 + assert "

Returned rows

" in response.text + assert "Only the first 10 returned rows are shown." in response.text + assert '' in response.text + assert '' in response.text + assert '' in response.text + assert '' in response.text + assert '' not in response.text + assert '' not in response.text @pytest.mark.parametrize(
idname1Cleo1Dog 1!10Dog 10!11Dog 11!