Dependency Injection#
The SQLSpec plugin integrates with Litestar's dependency injection system. By default,
it provides a session under the key db_session. You can customize this key or register
multiple databases with distinct keys.
Default Injection#
When you add SQLSpecPlugin to your app, handlers can request db_session to receive
a session scoped to the request:
from sqlspec.adapters.aiosqlite import AiosqliteDriver
@get("/users")
async def list_users(db_session: AiosqliteDriver) -> list[User]:
result = await db_session.execute("SELECT * FROM users")
return result.all(schema_type=User)
Note
Use the driver type that matches your config. For example, SqliteDriver for
SqliteConfig, AiosqliteDriver for AiosqliteConfig, or AsyncpgDriver
for AsyncpgConfig.
Custom Keys#
Use extension_config to customize the dependency injection keys for each database.
Set session_key, connection_key, and pool_key to unique values when using multiple databases.
dependency keys#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
sqlspec = SQLSpec()
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"}, extension_config={"litestar": {"session_key": "analytics"}}
),
name="analytics",
)
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"}, extension_config={"litestar": {"session_key": "primary"}}
),
name="primary",
)
Multiple Databases#
Configure each database with unique keys in its extension_config, then use a single plugin.
You can mix async and sync adapters - for example, an async PostgreSQL primary database with a
sync DuckDB for ETL operations:
from litestar import Litestar, get
from sqlspec import SQLSpec
from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgDriver
from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBDriver
from sqlspec.extensions.litestar import SQLSpecPlugin
sqlspec = SQLSpec()
# Primary async PostgreSQL database
sqlspec.add_config(
AsyncpgConfig(
connection_config={
"host": "localhost",
"port": 5432,
"database": "app",
"user": "app",
"password": "secret",
},
extension_config={"litestar": {"session_key": "db"}}
)
)
# ETL sync DuckDB database with custom keys
sqlspec.add_config(
DuckDBConfig(
connection_config={"database": "/tmp/etl.db"},
extension_config={
"litestar": {
"session_key": "etl_db",
"connection_key": "etl_connection",
"pool_key": "etl_pool",
}
}
)
)
@get("/report")
async def report(db: AsyncpgDriver, etl_db: DuckDBDriver) -> dict:
# Async query to primary PostgreSQL
users = await db.select("SELECT * FROM users")
# Sync query to DuckDB ETL database
metrics = etl_db.select("SELECT * FROM analytics")
return {"users": users.all(), "metrics": metrics.all()}
app = Litestar(
route_handlers=[report],
plugins=[SQLSpecPlugin(sqlspec=sqlspec)] # Single plugin handles all configs
)
Config Lookup Outside App Construction#
SQLSpecPlugin.get_config() accepts two kinds of identifier, and they become
available at different points in the application lifecycle:
Registry identities - a config instance, its concrete config type, or its
bind_key. These exist as soon as the plugin is constructed, so composition roots, CLI commands, and background workers can resolve a config without building a Litestar application first.Litestar dependency identities -
session_key,connection_key, andpool_key. These name dependency providers generated during plugin registration, so they resolve only after the plugin has been added to aLitestarapplication.
from litestar import Litestar
from sqlspec import SQLSpec
from sqlspec.adapters.asyncpg import AsyncpgConfig
from sqlspec.extensions.litestar import SQLSpecPlugin
sqlspec = SQLSpec()
sqlspec.add_config(
AsyncpgConfig(
bind_key="primary",
connection_config={
"host": "localhost",
"port": 5432,
"database": "app",
"user": "app",
"password": "secret",
},
)
)
plugin = SQLSpecPlugin(sqlspec=sqlspec)
# Registry identity: resolves before the app exists
primary = plugin.get_config("primary")
app = Litestar(route_handlers=[], plugins=[plugin])
# Dependency identity: resolves only after registration
session_config = plugin.get_config("db_session")
Give each database a distinct bind_key when several share the same config class.
A config type resolves only when exactly one configuration uses it; a repeated type
raises KeyError listing the candidate bind keys so you can pick the right one.
A bind_key takes precedence over a dependency key of the same value in
get_config(), while the request-scoped accessors always read strings as
dependency keys.
Advanced DuckDB Configuration#
DuckDB supports extensions and connection hooks for advanced use cases like attaching
external PostgreSQL databases. Use driver_features to configure extensions and
on_connection_create for custom connection initialization:
from typing import Any
from sqlspec import SQLSpec
from sqlspec.adapters.duckdb import DuckDBConfig, DuckDBExtensionConfig
def on_connection_create(connection: Any) -> None:
"""Configure DuckDB connection with PostgreSQL attachment."""
# Load postgres extension and attach external database
connection.execute("LOAD postgres")
connection.execute(
"ATTACH 'dbname=app user=app password=secret host=localhost' "
"AS pg (TYPE POSTGRES, SCHEMA 'public')"
)
sqlspec = SQLSpec()
sqlspec.add_config(
DuckDBConfig(
connection_config={
"database": "/tmp/analytics.db",
"temp_directory": "/tmp",
},
driver_features={
"extensions": [
DuckDBExtensionConfig(name="postgres"),
DuckDBExtensionConfig(name="encodings"),
],
"on_connection_create": on_connection_create,
},
extension_config={
"litestar": {
"session_key": "etl_db",
"connection_key": "etl_connection",
}
}
)
)
This pattern enables querying PostgreSQL tables directly from DuckDB SQL:
@get("/sync-users")
def sync_users(etl_db: DuckDBDriver) -> dict:
# Query PostgreSQL via DuckDB's postgres extension
result = etl_db.execute(
"INSERT INTO local_users SELECT * FROM pg.users RETURNING *"
)
return {"synced": result.rowcount}