With num_sql_threads set to 0, await db.execute_write_fn(fn, block=False) raises TypeError: cannot unpack non-iterable NoneType object instead of returning a UUID.
The docs describe block=False as fire-and-forget returning a task id:
If you specify block=False the method becomes fire-and-forget [...] A UUID representing the queued task will be returned.
Cause
In datasette/database.py, execute_write_fn takes a non-threaded branch when there is no executor, so result is whatever fn returned:
if self.ds.executor is None:
# non-threaded mode
...
result = fn(self._write_connection)
else:
result = await self._send_to_write_thread(fn, block=block, transaction=transaction)
The block=False path then unpacks result unconditionally:
else:
# For non-blocking writes, spawn a background task ...
task_id, reply_future = result
Only _send_to_write_thread returns the (task_id, reply_future) pair. In the non-threaded branch result is the function's return value, which for an ordinary write function is None.
Reproduction
Against e889403d3bbe143854262682161c98a57bdb6594:
import asyncio
from datasette.app import Datasette
async def main():
ds = Datasette(memory=True, settings={"num_sql_threads": 0})
await ds.invoke_startup()
db = ds.add_memory_database("repro")
def make_table(conn):
conn.execute("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v TEXT)")
await db.execute_write_fn(make_table, block=True) # fine
def w(conn):
conn.execute("INSERT INTO t (v) VALUES ('a')") # returns None
await db.execute_write_fn(w, block=False) # TypeError
asyncio.run(main())
TypeError: cannot unpack non-iterable NoneType object
block=True works in the same configuration, and both work with the default num_sql_threads.
Scope, and what I am not claiming
- This only affects
num_sql_threads = 0. The default configuration is unaffected.
- I have not found an in-core caller that hits this, so I am not claiming a user-facing break in shipped Datasette. It is reachable from the documented plugin API.
- I have not tested this under Pyodide itself, only with the setting that selects the same branch.
- No security or data-loss impact is claimed. The write is applied before the unpack raises, so the failure is in the return path, not the write.
Possible fix
Return a task id in the non-threaded branch too, so the documented contract holds, or skip the unpack when there is no executor. I did not want to guess which shape you would prefer, and I am happy to send a PR with tests once you say which.
Disclosure: I used an AI coding assistant while investigating this. The reproduction above was run and confirmed by me on the stated commit.
With
num_sql_threadsset to0,await db.execute_write_fn(fn, block=False)raisesTypeError: cannot unpack non-iterable NoneType objectinstead of returning a UUID.The docs describe
block=Falseas fire-and-forget returning a task id:Cause
In
datasette/database.py,execute_write_fntakes a non-threaded branch when there is no executor, soresultis whateverfnreturned:The
block=Falsepath then unpacksresultunconditionally:Only
_send_to_write_threadreturns the(task_id, reply_future)pair. In the non-threaded branchresultis the function's return value, which for an ordinary write function isNone.Reproduction
Against
e889403d3bbe143854262682161c98a57bdb6594:block=Trueworks in the same configuration, and both work with the defaultnum_sql_threads.Scope, and what I am not claiming
num_sql_threads = 0. The default configuration is unaffected.Possible fix
Return a task id in the non-threaded branch too, so the documented contract holds, or skip the unpack when there is no executor. I did not want to guess which shape you would prefer, and I am happy to send a PR with tests once you say which.
Disclosure: I used an AI coding assistant while investigating this. The reproduction above was run and confirmed by me on the stated commit.