mirror of
https://github.com/simonw/datasette.git
synced 2026-06-13 04:27:00 +02:00
Return rows/truncated from execute query if it used RETURNING
Refs https://github.com/simonw/datasette/issues/2762#issuecomment-4588066704
This commit is contained in:
parent
de8e072d96
commit
9b6f8ce2a7
3 changed files with 129 additions and 14 deletions
|
|
@ -355,11 +355,15 @@ class ExecuteWriteView(BaseView):
|
|||
status=ex.status,
|
||||
)
|
||||
|
||||
wants_json = _wants_json(request, is_json, data)
|
||||
try:
|
||||
cursor = await db.execute_write(sql, params, request=request)
|
||||
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)
|
||||
if _wants_json(request, is_json, data):
|
||||
if wants_json:
|
||||
return _block_framing(_error([message], 400))
|
||||
return await self._render_form(
|
||||
request,
|
||||
|
|
@ -378,17 +382,19 @@ class ExecuteWriteView(BaseView):
|
|||
message = "Query executed, {} row{} affected".format(
|
||||
cursor.rowcount, "" if cursor.rowcount == 1 else "s"
|
||||
)
|
||||
if _wants_json(request, is_json, data):
|
||||
return _block_framing(
|
||||
Response.json(
|
||||
{
|
||||
"ok": True,
|
||||
"message": message,
|
||||
"rowcount": cursor.rowcount,
|
||||
"analysis": _analysis_rows(analysis),
|
||||
}
|
||||
)
|
||||
)
|
||||
if wants_json:
|
||||
data = {
|
||||
"ok": True,
|
||||
"message": message,
|
||||
"rowcount": cursor.rowcount,
|
||||
"rows": [],
|
||||
"truncated": False,
|
||||
"analysis": _analysis_rows(analysis),
|
||||
}
|
||||
if cursor.description is not None:
|
||||
data["rows"] = [dict(row) for row in cursor.fetchall()]
|
||||
data["truncated"] = cursor.truncated
|
||||
return _block_framing(Response.json(data))
|
||||
|
||||
inserted_row_url = await _inserted_row_url(self.ds, db, analysis, cursor)
|
||||
execution_links = (
|
||||
|
|
|
|||
|
|
@ -554,7 +554,8 @@ Datasette analyzes the SQL before executing it. The actor must have ``execute-wr
|
|||
|
||||
Unsupported SQL operations are rejected by default. ``VACUUM`` is not allowed in arbitrary write SQL, and writes to SQLite virtual tables or shadow tables are rejected. SQL functions are allowed and are not separately restricted by Datasette permissions.
|
||||
|
||||
A successful response includes a message, the SQLite ``rowcount`` and a summary of the operations that were executed:
|
||||
A successful response includes a message, the SQLite ``rowcount``, a ``"rows"``
|
||||
list, a ``"truncated"`` flag and a summary of the operations that were executed:
|
||||
|
||||
The shape of the ``"analysis"`` block is not yet considered a stable API and may change in future Datasette releases.
|
||||
|
||||
|
|
@ -564,6 +565,8 @@ The shape of the ``"analysis"`` block is not yet considered a stable API and may
|
|||
"ok": true,
|
||||
"message": "Query executed, 1 row affected",
|
||||
"rowcount": 1,
|
||||
"rows": [],
|
||||
"truncated": false,
|
||||
"analysis": [
|
||||
{
|
||||
"operation": "insert",
|
||||
|
|
@ -577,6 +580,43 @@ The shape of the ``"analysis"`` block is not yet considered a stable API and may
|
|||
|
||||
If SQLite reports ``-1`` for the row count, the message will be ``"Query executed"``.
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"ok": true,
|
||||
"message": "Query executed, 1 row affected",
|
||||
"rowcount": 1,
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cleo"
|
||||
}
|
||||
],
|
||||
"truncated": false,
|
||||
"analysis": [
|
||||
{
|
||||
"operation": "insert",
|
||||
"database": "data",
|
||||
"table": "dogs",
|
||||
"required_permission": "insert-row, update-row, delete-row",
|
||||
"source": null
|
||||
},
|
||||
{
|
||||
"operation": "read",
|
||||
"database": "data",
|
||||
"table": "dogs",
|
||||
"required_permission": "view-table",
|
||||
"source": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Errors use the standard Datasette error format:
|
||||
|
||||
.. code-block:: json
|
||||
|
|
|
|||
|
|
@ -1884,10 +1884,79 @@ async def test_execute_write_post_requires_database_and_table_permissions():
|
|||
assert allowed.status_code == 200
|
||||
assert allowed.json()["ok"] is True
|
||||
assert allowed.json()["rowcount"] == 1
|
||||
assert allowed.json()["rows"] == []
|
||||
assert allowed.json()["truncated"] is False
|
||||
assert allowed.json()["analysis"][0]["operation"] == "insert"
|
||||
assert (await db.execute("select name from dogs")).first()[0] == "Cleo"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_write_json_includes_returning_rows():
|
||||
ds = Datasette(memory=True, default_deny=True)
|
||||
ds.root_enabled = True
|
||||
db = ds.add_memory_database("execute_write_returning_json", 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"},
|
||||
json={
|
||||
"sql": "insert into dogs (name) values (:name) returning id, name",
|
||||
"params": {"name": "Cleo"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
assert data["message"] == "Query executed, 1 row affected"
|
||||
assert data["rowcount"] == 1
|
||||
assert data["rows"] == [{"id": 1, "name": "Cleo"}]
|
||||
assert data["truncated"] is False
|
||||
assert [row["operation"] for row in data["analysis"]] == ["insert", "read"]
|
||||
assert (await db.execute("select id, name from dogs")).dicts() == [
|
||||
{"id": 1, "name": "Cleo"}
|
||||
]
|
||||
|
||||
|
||||
@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.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')"
|
||||
)
|
||||
await ds.invoke_startup()
|
||||
|
||||
response = await ds.client.post(
|
||||
"/data/-/execute-write",
|
||||
actor={"id": "root"},
|
||||
json={"sql": "update dogs set name = name || '!' returning id, name"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["ok"] is True
|
||||
assert data["message"] == "Query executed"
|
||||
assert data["rowcount"] == -1
|
||||
assert data["rows"] == [
|
||||
{"id": 1, "name": "Cleo!"},
|
||||
{"id": 2, "name": "Pancakes!"},
|
||||
]
|
||||
assert data["truncated"] is True
|
||||
assert (await db.execute("select count(*) from dogs where name like '%!'")).first()[
|
||||
0
|
||||
] == 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"database_name, sql",
|
||||
(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue