Skip to content

Commit c9e5425

Browse files
authored
feat!: refactor Litestar extension - remove wrapper classes, unify handlers (#96)
## Summary Major refactor of the Litestar extension that removes the `DatabaseConfig` wrapper class pattern, unifies duplicate handler files, and fixes type errors and test pollution issues. ## Breaking Changes **BREAKING**: The `DatabaseConfig` wrapper class has been removed. ### Before ```python from sqlspec.extensions.litestar import DatabaseConfig, SQLSpecPlugin from sqlspec.adapters.asyncpg import AsyncpgConfig asyncpg_config = AsyncpgConfig(pool_config={"dsn": "..."}) db_config = DatabaseConfig(config=asyncpg_config) # Wrapper! plugin = SQLSpecPlugin(configs=[db_config]) ``` ### After ```python from sqlspec import SQLSpec from sqlspec.extensions.litestar import SQLSpecPlugin from sqlspec.adapters.asyncpg import AsyncpgConfig asyncpg_config = AsyncpgConfig( pool_config={"dsn": "..."}, extension_config={"litestar": {"commit_mode": "autocommit"}} ) sql = SQLSpec() sql.add_config(asyncpg_config) plugin = SQLSpecPlugin(sqlspec=sql) ``` ## Migration Guide 1. **Remove `DatabaseConfig` wrapper**: - Pass core config classes directly to `SQLSpec` - Use `extension_config` field for Litestar-specific settings 2. **Update Litestar plugin initialization**: - Create `SQLSpec()` instance - Add configs with `sql.add_config()` - Pass `SQLSpec` instance to `SQLSpecPlugin(sqlspec=sql)` 3. **Move Litestar settings to `extension_config`**: ```python config = AsyncpgConfig( pool_config={"dsn": "..."}, extension_config={ "litestar": { "connection_key": "db_connection", "pool_key": "db_pool", "session_key": "db_session", "commit_mode": "autocommit" } } ) ``` ## Changes ### Removed DatabaseConfig Wrapper Class - **Deleted**: `sqlspec/extensions/litestar/config.py` (292 lines) - **Deleted**: `tests/unit/test_extensions/test_litestar/test_config.py` (482 lines) - Plugin now accepts `SQLSpec` instance directly - All configs from `SQLSpec` are automatically included ### Added extension_config Infrastructure - Added `extension_config` field to all core database config classes - Added `LitestarConfig` TypedDict for type-safe Litestar configuration - Configs read from `extension_config["litestar"]` namespace ### Unified Handler Implementation - **Merged**: `handlers_async.py` + `handlers_sync.py` → `handlers.py` - Single implementation with conditional `is_async` logic - Direct `await` for async drivers, `ensure_async_()` for sync drivers - Simplified plugin with single `_setup_handlers()` method ### Plugin Refactoring - Rewritten to work directly with core configs - Uses `config.is_async` to route to appropriate handlers - Eliminated wrapper overhead and complexity - Cleaner dependency injection setup ### Fixed Type Errors - Removed dead `pool_type` code - Fixed `"Never" is not awaitable` errors - Removed redundant `get_config()` delegation - All mypy and pyright errors resolved ### Fixed Test Database File Pollution - Tests no longer create 30+ database files in project root - Fixed SQLite URI mode handling (7 locations) - Converted hardcoded paths to use pytest `tmp_path` (5 locations) - Added defensive validation to SQLite config classes ### Code Quality Improvements - Removed defensive programming anti-patterns - Fixed nested import violations - Enforced CLAUDE.md standards throughout - All functions under 75-line limit ## Benefits - **Simpler API**: No wrapper classes, direct config usage - **Less Code**: Removed ~774 lines (wrapper + duplicate handlers) - **Type Safety**: mypy and pyright clean - **Better Architecture**: Plugin uses core configs directly - **Cleaner Tests**: No file pollution, proper temp directories ## Test Results ``` ✅ 14/14 Litestar handler tests passing ✅ 230/230 integration tests passing ✅ mypy: 0 errors ✅ pyright: 0 errors ✅ ruff: all checks passed ``` ## Files Changed ``` 24 files changed, 808 insertions(+), 1118 deletions(-) ``` **Major changes**: - Deleted wrapper class and duplicate handlers - Refactored plugin to use `SQLSpec` directly - Added `extension_config` infrastructure - Fixed all type errors and test pollution - Updated all examples and documentation ## Related - Supersedes portions of PR #83 - Includes merged PR #97 (handler unification + fixes)
1 parent b9f24f4 commit c9e5425

File tree

24 files changed

+808
-1118
lines changed

24 files changed

+808
-1118
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ benchmarks/
6262
.crush
6363
CRUSH.md
6464
*.md
65-
!README.md
66-
!CONTRIBUTING.md
65+
!./README.md
66+
!./CONTRIBUTING.md

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ install-uv: ## Install latest version of
4343
.PHONY: install
4444
install: destroy clean ## Install the project, dependencies, and pre-commit
4545
@echo "${INFO} Starting fresh installation..."
46-
@uv python pin 3.12 >/dev/null 2>&1
46+
@uv python pin 3.10 >/dev/null 2>&1
4747
@uv venv >/dev/null 2>&1
4848
@uv sync --all-extras --dev
4949
@echo "${OK} Installation complete! 🎉"
5050

5151
.PHONY: install-compiled
5252
install-compiled: destroy clean ## Install with mypyc compilation for performance
5353
@echo "${INFO} Starting fresh installation with mypyc compilation..."
54-
@uv python pin 3.12 >/dev/null 2>&1
54+
@uv python pin 3.10 >/dev/null 2>&1
5555
@uv venv >/dev/null 2>&1
5656
@echo "${INFO} Installing in editable mode with mypyc compilation..."
5757
@HATCH_BUILD_HOOKS_ENABLE=1 uv pip install -e .

docs/examples/litestar_asyncpg.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424

2525
from litestar import Litestar, get
2626

27-
from sqlspec import SQL
27+
from sqlspec import SQLSpec
2828
from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver, AsyncpgPoolConfig
29-
from sqlspec.extensions.litestar import DatabaseConfig, SQLSpec
29+
from sqlspec.core.statement import SQL
30+
from sqlspec.extensions.litestar import SQLSpecPlugin
3031

3132

3233
@get("/")
@@ -70,19 +71,17 @@ async def get_status() -> dict[str, str]:
7071

7172
# Configure SQLSpec with AsyncPG
7273
# Note: Modify this DSN to match your database configuration
73-
sqlspec = SQLSpec(
74-
config=[
75-
DatabaseConfig(
76-
config=AsyncpgConfig(
77-
pool_config=AsyncpgPoolConfig(
78-
dsn="postgresql://postgres:postgres@localhost:5433/postgres", min_size=5, max_size=5
79-
)
80-
),
81-
commit_mode="autocommit",
82-
)
83-
]
74+
sql = SQLSpec()
75+
sql.add_config(
76+
AsyncpgConfig(
77+
pool_config=AsyncpgPoolConfig(
78+
dsn="postgresql://postgres:postgres@localhost:5433/postgres", min_size=5, max_size=5
79+
),
80+
extension_config={"litestar": {"commit_mode": "autocommit"}},
81+
)
8482
)
85-
app = Litestar(route_handlers=[hello_world, get_version, list_tables, get_status], plugins=[sqlspec], debug=True)
83+
plugin = SQLSpecPlugin(sqlspec=sql)
84+
app = Litestar(route_handlers=[hello_world, get_version, list_tables, get_status], plugins=[plugin], debug=True)
8685

8786
if __name__ == "__main__":
8887
import os

docs/examples/litestar_duckllm.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
from litestar import Litestar, post
1717
from msgspec import Struct
1818

19+
from sqlspec import SQLSpec
1920
from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBDriver
20-
from sqlspec.extensions.litestar import SQLSpec
21+
from sqlspec.extensions.litestar import SQLSpecPlugin
2122

2223

2324
class ChatMessage(Struct):
@@ -30,8 +31,9 @@ def duckllm_chat(db_session: DuckDBDriver, data: ChatMessage) -> ChatMessage:
3031
return db_session.to_schema(results or {"message": "No response from DuckLLM"}, schema_type=ChatMessage)
3132

3233

33-
sqlspec = SQLSpec(
34-
config=DuckDBConfig(
34+
sql = SQLSpec()
35+
sql.add_config(
36+
DuckDBConfig(
3537
driver_features={
3638
"extensions": [{"name": "open_prompt"}],
3739
"secrets": [
@@ -48,7 +50,8 @@ def duckllm_chat(db_session: DuckDBDriver, data: ChatMessage) -> ChatMessage:
4850
}
4951
)
5052
)
51-
app = Litestar(route_handlers=[duckllm_chat], plugins=[sqlspec], debug=True)
53+
plugin = SQLSpecPlugin(sqlspec=sql)
54+
app = Litestar(route_handlers=[duckllm_chat], plugins=[plugin], debug=True)
5255

5356
if __name__ == "__main__":
5457
import uvicorn

docs/examples/litestar_multi_db.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515

1616
from litestar import Litestar, get
1717

18+
from sqlspec import SQLSpec
1819
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
1920
from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBDriver
2021
from sqlspec.core.statement import SQL
21-
from sqlspec.extensions.litestar import DatabaseConfig, SQLSpec
22+
from sqlspec.extensions.litestar import SQLSpecPlugin
2223

2324

2425
@get("/test", sync_to_thread=True)
@@ -35,22 +36,19 @@ async def simple_sqlite(db_session: AiosqliteDriver) -> dict[str, str]:
3536
return {"greeting": greeting["greeting"] if greeting is not None else "hi"}
3637

3738

38-
sqlspec = SQLSpec(
39-
config=[
40-
DatabaseConfig(config=AiosqliteConfig(), commit_mode="autocommit"),
41-
DatabaseConfig(
42-
config=DuckDBConfig(
43-
driver_features={
44-
"extensions": [{"name": "vss", "force_install": True}],
45-
"secrets": [{"secret_type": "s3", "name": "s3_secret", "value": {"key_id": "abcd"}}],
46-
}
47-
),
48-
connection_key="etl_connection",
49-
session_key="etl_session",
50-
),
51-
]
39+
sql = SQLSpec()
40+
sql.add_config(AiosqliteConfig(extension_config={"litestar": {"commit_mode": "autocommit"}}))
41+
sql.add_config(
42+
DuckDBConfig(
43+
driver_features={
44+
"extensions": [{"name": "vss", "force_install": True}],
45+
"secrets": [{"secret_type": "s3", "name": "s3_secret", "value": {"key_id": "abcd"}}],
46+
},
47+
extension_config={"litestar": {"connection_key": "etl_connection", "session_key": "etl_session"}},
48+
)
5249
)
53-
app = Litestar(route_handlers=[simple_sqlite, simple_select], plugins=[sqlspec])
50+
plugin = SQLSpecPlugin(sqlspec=sql)
51+
app = Litestar(route_handlers=[simple_sqlite, simple_select], plugins=[plugin])
5452

5553
if __name__ == "__main__":
5654
import os

docs/examples/litestar_psycopg.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,27 @@
1515

1616
from litestar import Litestar, get
1717

18+
from sqlspec import SQLSpec
1819
from sqlspec.adapters.psycopg import PsycopgAsyncConfig, PsycopgAsyncDriver
19-
from sqlspec.extensions.litestar import DatabaseConfig, SQLSpec
20+
from sqlspec.core.statement import SQL
21+
from sqlspec.extensions.litestar import SQLSpecPlugin
2022

2123

2224
@get("/")
2325
async def simple_psycopg(db_session: PsycopgAsyncDriver) -> dict[str, str]:
24-
from sqlspec.core.statement import SQL
25-
2626
result = await db_session.execute(SQL("SELECT 'Hello, world!' AS greeting"))
2727
return result.get_first() or {"greeting": "No result found"}
2828

2929

30-
sqlspec = SQLSpec(
31-
config=[
32-
DatabaseConfig(
33-
config=PsycopgAsyncConfig(
34-
pool_config={"conninfo": "postgres://app:app@localhost:15432/app", "min_size": 1, "max_size": 3}
35-
),
36-
commit_mode="autocommit",
37-
)
38-
]
30+
sql = SQLSpec()
31+
sql.add_config(
32+
PsycopgAsyncConfig(
33+
pool_config={"conninfo": "postgres://app:app@localhost:15432/app", "min_size": 1, "max_size": 3},
34+
extension_config={"litestar": {"commit_mode": "autocommit"}},
35+
)
3936
)
40-
app = Litestar(route_handlers=[simple_psycopg], plugins=[sqlspec])
37+
plugin = SQLSpecPlugin(sqlspec=sql)
38+
app = Litestar(route_handlers=[simple_psycopg], plugins=[plugin])
4139

4240
if __name__ == "__main__":
4341
import os

docs/examples/litestar_single_db.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from aiosqlite import Connection
99
from litestar import Litestar, get
1010

11+
from sqlspec import SQLSpec
1112
from sqlspec.adapters.aiosqlite import AiosqliteConfig
12-
from sqlspec.extensions.litestar import SQLSpec
13+
from sqlspec.extensions.litestar import SQLSpecPlugin
1314

1415

1516
@get("/")
@@ -23,5 +24,7 @@ async def simple_sqlite(db_connection: Connection) -> dict[str, str]:
2324
return {"greeting": next(iter(result))[0]}
2425

2526

26-
sqlspec = SQLSpec(config=AiosqliteConfig())
27-
app = Litestar(route_handlers=[simple_sqlite], plugins=[sqlspec])
27+
sql = SQLSpec()
28+
sql.add_config(AiosqliteConfig())
29+
plugin = SQLSpecPlugin(sqlspec=sql)
30+
app = Litestar(route_handlers=[simple_sqlite], plugins=[plugin])

sqlspec/adapters/aiosqlite/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ def __init__(
7979
if "database" not in config_dict or config_dict["database"] == ":memory:":
8080
config_dict["database"] = "file::memory:?cache=shared"
8181
config_dict["uri"] = True
82+
elif "database" in config_dict:
83+
database_path = str(config_dict["database"])
84+
if database_path.startswith("file:") and not config_dict.get("uri"):
85+
logger.debug(
86+
"Database URI detected (%s) but uri=True not set. "
87+
"Auto-enabling URI mode to prevent physical file creation.",
88+
database_path,
89+
)
90+
config_dict["uri"] = True
8291

8392
super().__init__(
8493
pool_config=config_dict,

sqlspec/adapters/sqlite/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""SQLite database configuration with thread-local connections."""
22

3+
import logging
34
import uuid
45
from contextlib import contextmanager
56
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
@@ -11,6 +12,8 @@
1112
from sqlspec.adapters.sqlite.pool import SqliteConnectionPool
1213
from sqlspec.config import SyncDatabaseConfig
1314

15+
logger = logging.getLogger(__name__)
16+
1417
if TYPE_CHECKING:
1518
from collections.abc import Generator
1619

@@ -64,6 +67,15 @@ def __init__(
6467
if "database" not in pool_config or pool_config["database"] == ":memory:":
6568
pool_config["database"] = f"file:memory_{uuid.uuid4().hex}?mode=memory&cache=private"
6669
pool_config["uri"] = True
70+
elif "database" in pool_config:
71+
database_path = str(pool_config["database"])
72+
if database_path.startswith("file:") and not pool_config.get("uri"):
73+
logger.debug(
74+
"Database URI detected (%s) but uri=True not set. "
75+
"Auto-enabling URI mode to prevent physical file creation.",
76+
database_path,
77+
)
78+
pool_config["uri"] = True
6779

6880
super().__init__(
6981
bind_key=bind_key,

sqlspec/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ def get_config(
161161
logger.debug("Retrieved configuration: %s", self._get_config_name(name))
162162
return config
163163

164+
@property
165+
def configs(self) -> "dict[type, DatabaseConfigProtocol[Any, Any, Any]]":
166+
"""Access the registry of database configurations.
167+
168+
Returns:
169+
Dictionary mapping config types to config instances.
170+
"""
171+
return self._configs
172+
164173
@overload
165174
def get_connection(
166175
self,

0 commit comments

Comments
 (0)