Skip to content

Commit 85f6c20

Browse files
committed
Fixes #80: disable DuckDB process worker
1 parent 78eb1dd commit 85f6c20

File tree

6 files changed

+134
-1
lines changed

6 files changed

+134
-1
lines changed

sqlit/domains/connections/providers/adapters/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ def supports_sequences(self) -> bool:
194194
"""
195195
return False
196196

197+
@property
198+
def supports_process_worker(self) -> bool:
199+
"""Whether this adapter supports running queries in a separate process."""
200+
return True
201+
197202
@property
198203
def test_query(self) -> str:
199204
"""A simple query to test the connection.

sqlit/domains/connections/providers/duckdb/adapter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ def supports_sequences(self) -> bool:
5555
"""DuckDB supports sequences."""
5656
return True
5757

58+
@property
59+
def supports_process_worker(self) -> bool:
60+
"""DuckDB file locking conflicts with multi-process connections."""
61+
return False
62+
5863
@property
5964
def default_schema(self) -> str:
6065
return "main"

sqlit/domains/explorer/ui/tree/loaders.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ async def work_async() -> None:
6666
use_worker = bool(getattr(runtime, "process_worker", False)) and not bool(
6767
getattr(getattr(runtime, "mock", None), "enabled", False)
6868
)
69+
if use_worker:
70+
try:
71+
from sqlit.domains.process_worker.app.support import supports_process_worker
72+
except Exception:
73+
pass
74+
else:
75+
provider = getattr(host, "current_provider", None)
76+
use_worker = supports_process_worker(provider)
6977
if use_worker and hasattr(host, "_get_process_worker_client_async"):
7078
client = await host._get_process_worker_client_async() # type: ignore[attr-defined]
7179
else:
@@ -159,6 +167,14 @@ async def work_async() -> None:
159167
use_worker = bool(getattr(runtime, "process_worker", False)) and not bool(
160168
getattr(getattr(runtime, "mock", None), "enabled", False)
161169
)
170+
if use_worker:
171+
try:
172+
from sqlit.domains.process_worker.app.support import supports_process_worker
173+
except Exception:
174+
pass
175+
else:
176+
provider = getattr(host, "current_provider", None)
177+
use_worker = supports_process_worker(provider)
162178
client = None
163179
if use_worker and hasattr(host, "_get_process_worker_client_async"):
164180
client = await host._get_process_worker_client_async() # type: ignore[attr-defined]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Helpers for deciding process worker availability."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
8+
def supports_process_worker(provider: Any) -> bool:
9+
"""Return False when a provider or adapter opts out of process workers."""
10+
adapter = getattr(provider, "connection_factory", None)
11+
for candidate in (adapter, provider):
12+
value = getattr(candidate, "supports_process_worker", None)
13+
if isinstance(value, bool):
14+
return value
15+
return True

sqlit/domains/process_worker/ui/mixins/process_worker_lifecycle.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ def _use_process_worker(self: QueryMixinHost, provider: Any) -> bool:
2222
return False
2323
if bool(getattr(getattr(runtime, "mock", None), "enabled", False)):
2424
return False
25-
return True
25+
try:
26+
from sqlit.domains.process_worker.app.support import supports_process_worker
27+
except Exception:
28+
return True
29+
return supports_process_worker(provider)
2630

2731
def _get_process_worker_client(self: QueryMixinHost) -> Any | None:
2832
client = getattr(self, "_process_worker_client", None)

tests/test_duckdb.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
from __future__ import annotations
44

5+
import sys
6+
from pathlib import Path
7+
8+
import pytest
9+
510
from .test_database_base import BaseDatabaseTestsWithLimit, DatabaseTestConfig
611

712

@@ -128,3 +133,86 @@ def test_query_duckdb_invalid_query(self, duckdb_connection, cli_runner):
128133
)
129134
# Should fail gracefully
130135
assert result.returncode != 0 or "error" in result.stdout.lower() or "error" in result.stderr.lower()
136+
137+
138+
@pytest.mark.parametrize("use_worker", [True, False])
139+
def test_duckdb_process_worker_no_lock(duckdb_db, use_worker):
140+
"""Ensure DuckDB queries work with or without the process worker."""
141+
if sys.platform.startswith("win"):
142+
pytest.skip("DuckDB file-lock behavior differs on Windows")
143+
144+
from sqlit.domains.connections.app.session import ConnectionSession
145+
from sqlit.domains.connections.domain.config import ConnectionConfig, FileEndpoint
146+
from sqlit.domains.process_worker.app.process_worker_client import ProcessWorkerClient
147+
from sqlit.domains.process_worker.app.support import supports_process_worker
148+
from sqlit.domains.query.app.transaction import TransactionExecutor
149+
150+
config = ConnectionConfig(
151+
name="duckdb_lock_test",
152+
db_type="duckdb",
153+
endpoint=FileEndpoint(path=str(duckdb_db)),
154+
)
155+
156+
session = ConnectionSession.create(config)
157+
try:
158+
outcome = None
159+
use_worker_effective = bool(use_worker and supports_process_worker(session.provider))
160+
if use_worker_effective:
161+
client = ProcessWorkerClient()
162+
try:
163+
outcome = client.execute("SELECT 1", config, max_rows=1)
164+
finally:
165+
client.close()
166+
else:
167+
executor = TransactionExecutor(config=config, provider=session.provider)
168+
try:
169+
executor.execute("SELECT 1", max_rows=1)
170+
finally:
171+
executor.close()
172+
finally:
173+
session.close()
174+
175+
if use_worker_effective:
176+
assert outcome is not None
177+
assert outcome.error is None, f"Unexpected error: {outcome.error}"
178+
179+
180+
def test_duckdb_schema_service_repo_file(tmp_path):
181+
"""Validate explorer schema service against the repo DuckDB fixture."""
182+
if sys.platform.startswith("win"):
183+
pytest.skip("DuckDB file-lock behavior differs on Windows")
184+
185+
from sqlit.domains.connections.app.session import ConnectionSession
186+
from sqlit.domains.connections.domain.config import ConnectionConfig, FileEndpoint
187+
from sqlit.domains.explorer.app.schema_service import ExplorerSchemaService
188+
try:
189+
import duckdb # type: ignore
190+
except Exception:
191+
pytest.skip("duckdb is not installed")
192+
193+
db_path = Path(tmp_path) / "cats.duckdb"
194+
conn = duckdb.connect(str(db_path))
195+
conn.execute("CREATE TABLE cats (id INTEGER, name VARCHAR, motto VARCHAR)")
196+
conn.execute("INSERT INTO cats VALUES (1, 'Mochi', 'Nap hard, snack harder')")
197+
conn.execute("INSERT INTO cats VALUES (2, 'Nimbus', 'Gravity is optional')")
198+
conn.close()
199+
200+
config = ConnectionConfig(
201+
name="duckdb_repo_fixture",
202+
db_type="duckdb",
203+
endpoint=FileEndpoint(path=str(db_path)),
204+
)
205+
206+
session = ConnectionSession.create(config)
207+
try:
208+
service = ExplorerSchemaService(session=session, object_cache={})
209+
tables = service.list_folder_items("tables", None)
210+
table_names = {name for kind, _, name in tables if kind == "table"}
211+
assert "cats" in table_names
212+
213+
schema = session.provider.capabilities.default_schema or "main"
214+
columns = service.list_columns(None, schema, "cats")
215+
column_names = {col.name for col in columns}
216+
assert {"id", "name", "motto"}.issubset(column_names)
217+
finally:
218+
session.close()

0 commit comments

Comments
 (0)