Repository: simonw/datasette

Issue:
Datasette setting to disable threading (for Pyodide)
> I'm going to add a Datasette setting to disable threading entirely, designed for usage in this particular case.
>
> I thought about adding a new setting, then I noticed this:
>
>     datasette mydatabase.db --setting num_sql_threads 10
>
> I'm going to let users set that to `0` to disable threaded execution of SQL queries.

_Originally posted by @simonw in https://github.com/simonw/datasette/issues/1733#issuecomment-1115278325_

Comments:

Patch:
diff --git a/datasette/app.py b/datasette/app.py
index a5330458..b7b84371 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -288,9 +288,12 @@ class Datasette:
         self._settings = dict(DEFAULT_SETTINGS, **(settings or {}))
         self.renderers = {}  # File extension -> (renderer, can_render) functions
         self.version_note = version_note
-        self.executor = futures.ThreadPoolExecutor(
-            max_workers=self.setting("num_sql_threads")
-        )
+        if self.setting("num_sql_threads") == 0:
+            self.executor = None
+        else:
+            self.executor = futures.ThreadPoolExecutor(
+                max_workers=self.setting("num_sql_threads")
+            )
         self.max_returned_rows = self.setting("max_returned_rows")
         self.sql_time_limit_ms = self.setting("sql_time_limit_ms")
         self.page_size = self.setting("default_page_size")
@@ -862,6 +865,8 @@ class Datasette:
         ]
 
     def _threads(self):
+        if self.setting("num_sql_threads") == 0:
+            return {"num_threads": 0, "threads": []}
         threads = list(threading.enumerate())
         d = {
             "num_threads": len(threads),
diff --git a/datasette/database.py b/datasette/database.py
index ba594a8c..44d32667 100644
--- a/datasette/database.py
+++ b/datasette/database.py
@@ -45,6 +45,9 @@ class Database:
         self._cached_table_counts = None
         self._write_thread = None
         self._write_queue = None
+        # These are used when in non-threaded mode:
+        self._read_connection = None
+        self._write_connection = None
         if not self.is_mutable and not self.is_memory:
             p = Path(path)
             self.hash = inspect_hash(p)
@@ -134,6 +137,14 @@ class Database:
         return results
 
     async def execute_write_fn(self, fn, block=True):
+        if self.ds.executor is None:
+            # non-threaded mode
+            if self._write_connection is None:
+                self._write_connection = self.connect(write=True)
+                self.ds._prepare_connection(self._write_connection, self.name)
+            return fn(self._write_connection)
+
+        # threaded mode
         task_id = uuid.uuid5(uuid.NAMESPACE_DNS, "datasette.io")
         if self._write_queue is None:
             self._write_queue = queue.Queue()
@@ -177,6 +188,14 @@ class Database:
             task.reply_queue.sync_q.put(result)
 
     async def execute_fn(self, fn):
+        if self.ds.executor is None:
+            # non-threaded mode
+            if self._read_connection is None:
+                self._read_connection = self.connect()
+                self.ds._prepare_connection(self._read_connection, self.name)
+            return fn(self._read_connection)
+
+        # threaded mode
         def in_thread():
             conn = getattr(connections, self.name, None)
             if not conn:
diff --git a/docs/settings.rst b/docs/settings.rst
index 60c4b36d..8437fb04 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -107,6 +107,8 @@ Maximum number of threads in the thread pool Datasette uses to execute SQLite qu
 
     datasette mydatabase.db --setting num_sql_threads 10
 
+Setting this to 0 turns off threaded SQL queries entirely - useful for environments that do not support threading such as `Pyodide <https://pyodide.org/>`__.
+
 .. _setting_allow_facet:
 
 allow_facet


Test patch:
diff --git a/tests/test_internals_datasette.py b/tests/test_internals_datasette.py
index cc200a2d..1dc14cab 100644
--- a/tests/test_internals_datasette.py
+++ b/tests/test_internals_datasette.py
@@ -1,7 +1,7 @@
 """
 Tests for the datasette.app.Datasette class
 """
-from datasette.app import Datasette
+from datasette.app import Datasette, Database
 from itsdangerous import BadSignature
 from .fixtures import app_client
 import pytest
@@ -63,3 +63,15 @@ async def test_datasette_constructor():
             "hash": None,
         }
     ]
+
+
+@pytest.mark.asyncio
+async def test_num_sql_threads_zero():
+    ds = Datasette([], memory=True, settings={"num_sql_threads": 0})
+    db = ds.add_database(Database(ds, memory_name="test_num_sql_threads_zero"))
+    await db.execute_write("create table t(id integer primary key)")
+    await db.execute_write("insert into t (id) values (1)")
+    response = await ds.client.get("/-/threads.json")
+    assert response.json() == {"num_threads": 0, "threads": []}
+    response2 = await ds.client.get("/test_num_sql_threads_zero/t.json?_shape=array")
+    assert response2.json() == [{"id": 1}]