Creating Adapters#
This guide explains how to build and contribute a database adapter for SQLSpec. SQLSpec provides a type-safe SQL query execution and result-mapping layer designed for minimal abstraction without ORM semantics. Adapters connect SQLSpec's core statement processing pipeline—including SQLGlot AST transformation, dialect conversions, and parameter extraction—to a concrete database driver.
Overview#
An adapter in SQLSpec bridges two layers:
The SQLSpec Pipeline: Transforms user queries, optimizes AST representations, normalizes parameters, and maps results to dictionaries, tuples, dataclasses, msgspec Structs, Pydantic models, or Arrow tables.
The Database Driver: Manages physical connections, cursor lifecycles, statement execution, and transaction boundaries.
Adapters are modular and live under sqlspec/adapters/<adapter_name>/.
Sync vs. Async Architecture#
SQLSpec supports both synchronous and asynchronous database drivers through dedicated base classes:
Synchronous Adapters: - Configuration subclasses
SyncDatabaseConfig. - Driver subclassesSyncDriverAdapterBase. - Data dictionary subclassesSyncDataDictionaryBase. - Exception handler subclassesBaseSyncExceptionHandler.Asynchronous Adapters: - Configuration subclasses
AsyncDatabaseConfig. - Driver subclassesAsyncDriverAdapterBase. - Data dictionary subclassesAsyncDataDictionaryBase. - Exception handler subclassesBaseAsyncExceptionHandler.
Both modes expose the same high-level query operations (execute(), select(), select_value(), select_to_arrow(), select_stream(), execute_many()), with asynchronous operations defined as awaitable coroutines and async context managers.
Adapter Module Layout#
Every database adapter is organized into a standardized directory structure:
sqlspec/adapters/<name>/
├── __init__.py # Re-exports the public adapter interface
├── _typing.py # Local type aliases for connection and cursor types
├── config.py # Typed configuration, connection params, and driver features
├── core.py # Statement config, apply_driver_features, exception mapping
├── driver.py # Driver adapter implementation and exception handling
├── pool.py # Connection pool implementation or driver pool wrapper
├── data_dictionary.py # Database schema introspection (tables, columns, indexes, FKs)
└── type_converter.py # Optional database-specific type conversion and serialization
Module Responsibilities#
config.py#
Defines the adapter configuration and connection parameters:
Subclasses
SyncDatabaseConfigorAsyncDatabaseConfig.Declares a
<Name>ConnectionParamsTypedDictfor driver-specific connection arguments.Declares a
<Name>DriverFeaturesTypedDictfor optional feature flags.Implements
provide_connection(),provide_pool(), andprovide_session().
core.py#
Houses compiled and core helpers for statement execution:
default_statement_config: Configures the SQLGlot dialect, parameter style (named,qmark,numeric, orpyformat), and type coercions.apply_driver_features(statement_config, driver_features): Merges feature defaults and applies parameter serializer customizations, returning atuple[StatementConfig, dict[str, Any]].create_mapped_exception(error, *, logger=None): Translates native database driver exceptions into the unifiedsqlspec.exceptionshierarchy.
driver.py#
Implements the driver adapter by subclassing SyncDriverAdapterBase or AsyncDriverAdapterBase. The driver must implement:
dispatch_execute(): Executes a single SQL statement with parameters and returns anExecutionResult.dispatch_execute_many(): Executes a statement across multiple parameter batches.dispatch_execute_script(): Executes raw SQL scripts or DDL.begin(),commit(),rollback(): Manages transaction boundaries.with_cursor(): Context manager yielding a cursor for database operations.handle_database_exceptions(): Context manager wrapping operations in the adapter's exception handler._connection_in_transaction(): Inspects whether the active connection is inside a transaction.data_dictionary: Returns the data dictionary instance for schema introspection.
data_dictionary.py#
Provides schema introspection capabilities by subclassing SyncDataDictionaryBase or AsyncDataDictionaryBase:
get_tables(): Retrieves table metadata for a given schema.get_columns(): Retrieves column metadata for a table or schema.get_indexes(): Retrieves index metadata.get_foreign_keys(): Retrieves foreign key relationships.get_version(): Queries database version information.get_feature_flag(): Checks feature availability on the target database engine.
pool.py#
Manages connection pooling, wrapping either the driver's native connection pool or implementing SQLSpec's connection pool protocols.
__init__.py#
Re-exports the public API of the adapter, including configuration classes, connection parameters, driver classes, and the default statement configuration.
The Driver Features Pattern#
SQLSpec standardizes adapter capabilities through driver feature dictionaries. Features allow callers to toggle adapter behaviors such as custom type adapters, JSON serialization, UUID conversion, or native bulk-loading optimizations:
from typing import Any, TypedDict
from typing_extensions import NotRequired
class ExampleDriverFeatures(TypedDict):
enable_custom_adapters: NotRequired[bool]
enable_uuid_conversion: NotRequired[bool]
json_serializer: NotRequired[Any]
json_deserializer: NotRequired[Any]
from typing import Any, TypedDict
from typing import NotRequired
class ExampleDriverFeatures(TypedDict):
enable_custom_adapters: NotRequired[bool]
enable_uuid_conversion: NotRequired[bool]
json_serializer: NotRequired[Any]
json_deserializer: NotRequired[Any]
In core.py, the apply_driver_features() helper normalizes these options and returns an updated statement configuration:
def apply_driver_features(
statement_config: StatementConfig,
driver_features: Mapping[str, Any] | None,
) -> tuple[StatementConfig, dict[str, Any]]:
features = dict(driver_features) if driver_features else {}
features.setdefault("enable_custom_adapters", False)
features.setdefault("enable_uuid_conversion", True)
# Update statement_config with any custom serializers or settings
return statement_config, features
Exception Mapping#
Adapters must never leak raw driver exceptions to application callers. Every database exception is mapped to a subclass of SQLSpecError via create_mapped_exception():
from sqlspec.exceptions import (
OperationalError,
SQLSpecError,
)
def create_mapped_exception(
error: Exception,
*,
logger: Any = None,
) -> SQLSpecError:
"""Map driver-specific exception to a SQLSpec exception."""
# Inspect driver error codes or error hierarchy and return the matching SQLSpecError
return OperationalError(str(error))
Adapter Skeleton#
Below is a minimal synchronous driver implementation illustrating the required dispatch and transaction methods:
class ExampleExceptionHandler(BaseSyncExceptionHandler):
"""Adapter-specific exception handler wrapping database errors."""
class ExampleDriver(SyncDriverAdapterBase):
"""Example synchronous database driver adapter."""
@property
def data_dictionary(self) -> "SyncDataDictionaryBase":
"""Return the data dictionary instance for schema inspection.
Returns:
Data dictionary instance.
"""
raise NotImplementedError
def dispatch_execute(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
"""Execute a single statement and return execution metadata.
Args:
cursor: Database cursor.
statement: SQL statement to execute.
Returns:
Execution result metadata.
"""
raise NotImplementedError
def dispatch_execute_many(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
"""Execute a statement with multiple parameter sets.
Args:
cursor: Database cursor.
statement: SQL statement with batched parameters.
Returns:
Execution result metadata.
"""
raise NotImplementedError
def begin(self) -> None:
"""Begin a transaction on the current connection."""
raise NotImplementedError
def commit(self) -> None:
"""Commit the current transaction."""
raise NotImplementedError
def rollback(self) -> None:
"""Roll back the current transaction."""
raise NotImplementedError
@contextmanager
def with_cursor(self, connection: Any) -> Generator[Any, None, None]:
"""Acquire and yield a cursor from the connection.
Args:
connection: Active database connection.
Yields:
Database cursor.
"""
cursor = connection.cursor()
try:
yield cursor
finally:
cursor.close()
def handle_database_exceptions(self) -> ExampleExceptionHandler:
"""Provide the exception handler context manager.
Returns:
Exception handler instance.
"""
return ExampleExceptionHandler()
def _connection_in_transaction(self) -> bool:
"""Check whether the active connection is in an open transaction.
Returns:
True if in a transaction, False otherwise.
"""
return False
Testing Guidelines#
All adapter contributions must include comprehensive test coverage following the repository's test placement structure:
Unit Tests (
tests/unit/adapters/<adapter>/): - Test configuration instantiation, parameter normalization, statement compilation, and feature flag merging. - Unit tests must run without requiring live database services.Contract Tests (
tests/integration/adapters/<family>/test_shared.py): - Reusable test contracts verify multi-driver behavioral parity across database families (e.g. SQLite, PostgreSQL, MySQL, Oracle, MSSQL).Integration Tests (
tests/integration/adapters/<family>/<driver>/): - Live database tests usingpytest-databasesfixtures. - Test connection pooling, transactional commit and rollback, savepoints, batch execution, and Arrow table streaming.
Quality Verification#
Before opening a pull request for a new adapter, ensure all quality gates pass locally:
# Run linters, formatters, Prek hooks, and slotscheck
make lint
# Run static type checking (mypy + pyright)
make type-check
# Run tests
make test
# Verify test coverage (changed code must maintain >= 90% coverage)
make coverage
See Also#
Driver for the complete driver protocol and base class references.
Adapters for existing adapter configuration reference.
Contribution Guide for repository setup, commit standards, and PR workflows.