Skip to content

execute_write_fn(block=False) raises TypeError when num_sql_threads is 0 #2859

Description

@HarperZ9

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions