Add a default execution time limit to writes

This commit is contained in:
Simon Willison 2026-09-08 21:14:32 -07:00
commit ac2a9a43a5
3 changed files with 21 additions and 4 deletions

View file

@ -247,17 +247,29 @@ class Database:
return_all=False,
returning_limit=EXECUTE_WRITE_RETURNING_LIMIT,
transaction=True,
time_limit_ms=2000,
):
self._check_not_closed()
if returning_limit < 0:
raise ValueError("returning_limit must be >= 0")
def _inner(conn):
def execute_sql(conn):
cursor = conn.execute(sql, params or [])
return ExecuteWriteResult.from_cursor(
cursor, return_all=return_all, returning_limit=returning_limit
)
def _inner(conn):
try:
if time_limit_ms is None:
return execute_sql(conn)
with sqlite_timelimit(conn, time_limit_ms):
return execute_sql(conn)
except (sqlite3.OperationalError, sqlite3.DatabaseError) as e:
if e.args == ("interrupted",):
raise QueryInterrupted(e, sql, params)
raise
with trace("sql", database=self.name, sql=sql.strip(), params=params):
results = await self.execute_write_fn(
_inner, block=block, request=request, transaction=transaction

View file

@ -1,6 +1,7 @@
import re
from urllib.parse import urlencode
from datasette.database import QueryInterrupted
from datasette.resources import DatabaseResource
from datasette.utils import UNSTABLE_API_MESSAGE, sqlite3
from datasette.utils.asgi import Response
@ -384,7 +385,7 @@ class ExecuteWriteView(BaseView):
try:
execute_write_kwargs = {"request": request}
cursor = await db.execute_write(sql, params, **execute_write_kwargs)
except sqlite3.DatabaseError as ex:
except (QueryInterrupted, sqlite3.DatabaseError) as ex:
message = str(ex)
if wants_json:
return _block_framing(Response.error([message], 400))

View file

@ -2023,8 +2023,8 @@ Example usage:
.. _database_execute_write:
await db.execute_write(sql, params=None, block=True, request=None, return_all=False, returning_limit=10, transaction=True)
--------------------------------------------------------------------------------------------------------------------------
await db.execute_write(sql, params=None, block=True, request=None, return_all=False, returning_limit=10, transaction=True, time_limit_ms=2000)
----------------------------------------------------------------------------------------------------------------------------------------------
SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
@ -2063,6 +2063,10 @@ Each call to ``execute_write()`` will be executed inside a transaction. Pass
``transaction=False`` for statements such as ``VACUUM`` that cannot run inside
a transaction.
Write statements have a default time limit of 2,000ms. Pass a different value
using ``time_limit_ms=`` or use ``time_limit_ms=None`` to allow the statement to
run without a time limit.
.. _database_execute_write_script:
await db.execute_write_script(sql, block=True)