mirror of
https://github.com/simonw/datasette.git
synced 2026-06-13 04:27:00 +02:00
INSERT ... RETURNING shows rows in /-/execute-write
Screenshot: https://github.com/simonw/datasette/issues/2762#issuecomment-4588111545
This commit is contained in:
parent
9b6f8ce2a7
commit
c9e5115044
6 changed files with 138 additions and 37 deletions
20
datasette/templates/_query_results.html
Normal file
20
datasette/templates/_query_results.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{% if display_rows %}
|
||||
<div class="table-wrapper"><table class="rows-and-columns">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in columns %}<th class="col-{{ column|to_css_class }}" scope="col">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in display_rows %}
|
||||
<tr>
|
||||
{% for column, td in zip(columns, row) %}
|
||||
<td class="col-{{ column|to_css_class }}">{{ td }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% elif show_zero_results %}
|
||||
<p class="zero-results">0 results</p>
|
||||
{% endif %}
|
||||
|
|
@ -81,6 +81,17 @@ form.sql.core input[data-execute-write-submit]:disabled {
|
|||
<p class="{% if execution_ok %}message-info{% else %}message-error{% endif %}">{{ execution_message }}{% for link in execution_links %} <a href="{{ link.href }}">{{ link.label }}</a>{% endfor %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if execute_write_returns_rows %}
|
||||
<h2>Returned rows</h2>
|
||||
{% if execute_write_truncated %}
|
||||
<p class="message-warning">Only the first {{ "{:,}".format(execute_write_display_rows|length) }} returned rows are shown.</p>
|
||||
{% endif %}
|
||||
{% set columns = execute_write_columns %}
|
||||
{% set display_rows = execute_write_display_rows %}
|
||||
{% set show_zero_results = true %}
|
||||
{% include "_query_results.html" %}
|
||||
{% endif %}
|
||||
|
||||
<form class="sql core" action="{{ urls.database(database) }}/-/execute-write" method="post" data-analyze-url="{{ urls.database(database) }}/-/execute-write/analyze">
|
||||
{% if write_template_tables %}
|
||||
<div class="execute-write-template-menu">
|
||||
|
|
|
|||
|
|
@ -73,27 +73,9 @@
|
|||
|
||||
{% if display_rows %}
|
||||
<p class="export-links">This data as {% for name, url in renderers.items() %}<a href="{{ url }}">{{ name }}</a>{{ ", " if not loop.last }}{% endfor %}, <a href="{{ url_csv }}">CSV</a></p>
|
||||
<div class="table-wrapper"><table class="rows-and-columns">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for column in columns %}<th class="col-{{ column|to_css_class }}" scope="col">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in display_rows %}
|
||||
<tr>
|
||||
{% for column, td in zip(columns, row) %}
|
||||
<td class="col-{{ column|to_css_class }}">{{ td }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{% else %}
|
||||
{% if not stored_query_write and not error %}
|
||||
<p class="zero-results">0 results</p>
|
||||
{% 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" %}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "<h2>Returned rows</h2>" in response.text
|
||||
assert '<table class="rows-and-columns">' in response.text
|
||||
assert '<th class="col-id" scope="col">id</th>' in response.text
|
||||
assert '<th class="col-name" scope="col">name</th>' in response.text
|
||||
assert '<td class="col-id">1</td>' in response.text
|
||||
assert '<td class="col-name">Cleo</td>' in response.text
|
||||
|
||||
assert non_returning_response.status_code == 200
|
||||
assert "Query executed, 1 row affected" in non_returning_response.text
|
||||
assert "<h2>Returned rows</h2>" not in non_returning_response.text
|
||||
assert '<p class="zero-results">0 results</p>' 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 "<h2>Returned rows</h2>" in response.text
|
||||
assert "Only the first 10 returned rows are shown." in response.text
|
||||
assert '<td class="col-id">1</td>' in response.text
|
||||
assert '<td class="col-name">Dog 1!</td>' in response.text
|
||||
assert '<td class="col-id">10</td>' in response.text
|
||||
assert '<td class="col-name">Dog 10!</td>' in response.text
|
||||
assert '<td class="col-id">11</td>' not in response.text
|
||||
assert '<td class="col-name">Dog 11!</td>' not in response.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue