Drivers and Querying#
SQLSpec provides a unified driver interface across supported databases. This page covers the common execution patterns and points to adapter-specific configuration.
Supported Drivers (High Level)#
PostgreSQL: asyncpg, psycopg (sync/async), psqlpy, ADBC
CockroachDB: cockroach-asyncpg, cockroach-psycopg
SQLite: sqlite3, aiosqlite, ADBC
MySQL & MariaDB: aiomysql, asyncmy, mysql-connector, pymysql
SQL Server: mssql-python, pymssql, arrow-odbc
Analytics & Cloud: DuckDB, BigQuery, Spanner, Oracle, ADBC, arrow-odbc
Core Execution Pattern#
sqlite session#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
db_path = tmp_path / "driver.sqlite"
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": str(db_path)}))
with spec.provide_session(config) as session:
session.execute("create table if not exists health (id integer primary key)")
result = session.execute("select count(*) as total from health")
print(result.one())
Transactions#
SQLSpec drivers provide a transaction() context manager that commits on
successful block exit and rolls back when an exception is raised. If the connection
already has an open transaction, the block joins it instead of starting a new one.
async with config.provide_session() as session:
async with session.transaction():
await session.execute("INSERT INTO users (name) VALUES (:name)", name="Ada")
await session.execute("INSERT INTO audit (action) VALUES (:action)", action="user-created")
with config.provide_session() as session:
with session.transaction():
session.execute("INSERT INTO users (name) VALUES (:name)", name="Ada")
session.execute("INSERT INTO audit (action) VALUES (:action)", action="user-created")
Nested Transactions and Savepoints#
Nested transaction() blocks run inside a savepoint on the enclosing
transaction. If the inner block raises, only its work is rolled back, allowing
the outer transaction to catch the error, continue, and commit:
from sqlspec.exceptions import UniqueViolationError
with config.provide_session() as session:
with session.transaction():
session.execute("INSERT INTO users (name) VALUES (:name)", name="Ada")
try:
with session.transaction():
session.execute("INSERT INTO users (name) VALUES (:name)", name="Ada")
except UniqueViolationError:
pass
Adapters without savepoint support (DuckDB, BigQuery, Spanner, and ADBC connections
to DuckDB, BigQuery, or Snowflake) raise ImproperConfigurationError when a
nested block is entered.
Manual Transaction Control#
For fine-grained lifecycle control, you can call begin(), commit(), and
rollback() directly on the driver:
manual transaction#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
db_path = tmp_path / "transactions.db"
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": str(db_path)}))
with spec.provide_session(config) as session:
session.execute("create table if not exists ledger (id integer primary key, note text)")
session.begin()
session.execute("insert into ledger (note) values ('committed')")
session.commit()
session.begin()
session.execute("insert into ledger (note) values ('rolled back')")
session.rollback()
result = session.execute("select note from ledger order by id")
print(result.all())
Parameter Binding#
positional and named parameters#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
db_path = tmp_path / "params.db"
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": str(db_path)}))
with spec.provide_session(config) as session:
session.execute("select :status as status, :status as status_copy", status="active")
result = session.execute("select ? as value", 42)
print(result.one())
Schema Mapping#
Use the schema_type parameter on select(), select_one(),
select_one_or_none(), and select_stream() to map result rows directly to
dataclasses, msgspec Structs, Pydantic models, attrs classes, or TypedDict
shapes. When working with raw SQLResult objects from execute(), pass
schema_type to result.all(), result.one(), result.one_or_none(),
or result.get_data().
schema_type mapping#from dataclasses import dataclass
from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
@dataclass
class User:
id: int
name: str
db_path = tmp_path / "schema.db"
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": str(db_path)}))
with spec.provide_session(config) as session:
session.execute("create table users (id integer primary key, name text)")
session.execute("insert into users (name) values ('Alice'), ('Bob')")
# select returns list of dicts by default
rows = session.select("select id, name from users order by id")
print(rows) # [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
# Use schema_type to map rows to dataclass instances
users = session.select("select id, name from users order by id", schema_type=User)
print(users) # [User(id=1, name='Alice'), User(id=2, name='Bob')]
# select_one with schema_type
user = session.select_one("select id, name from users where name = ?", "Alice", schema_type=User)
print(user) # User(id=1, name='Alice')
# select_one_or_none returns None if no match
maybe_user = session.select_one_or_none("select id, name from users where name = ?", "Nobody", schema_type=User)
print(maybe_user) # None
Scalar Values and Pagination#
select_value()returns a single scalar from a one-row, one-column result (with optional type conversion viavalue_type).select_value_or_none()returnsNonewhen no rows match.select_with_total()returns a(data_rows, total_count)tuple for pagination.Drivers also provide
fetch(),fetch_one(),fetch_one_or_none(),fetch_value(),fetch_value_or_none(),fetch_with_total(), andfetch_stream()aliases matching asyncpg conventions.
scalar values, execute_many, and pagination#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
db_path = tmp_path / "batch.db"
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": str(db_path)}))
with spec.provide_session(config) as session:
session.execute("create table users (id integer primary key, name text, email text)")
# execute_many inserts multiple rows in a single call
session.execute_many(
"insert into users (name, email) values (?, ?)",
[("Alice", "[email protected]"), ("Bob", "[email protected]"), ("Charlie", "[email protected]")],
)
# select_value returns a single scalar value
count = session.select_value("select count(*) from users")
print(count) # 3
# select_value with type conversion
count_int = session.select_value("select count(*) from users", value_type=int)
print(count_int) # 3
# select_value_or_none returns None when no rows match
email = session.select_value_or_none("select email from users where name = ?", "Nobody")
print(email) # None
from sqlspec import SQL
query = SQL("select id, name from users").paginate(page=1, page_size=2)
data, total = session.select_with_total(query)
print(f"Page has {len(data)} rows, total matching: {total}")
Streaming Results#
select_stream() returns a context-managed stream of dict rows fetched in
bounded chunks using each driver's native streaming primitive. Adapters without
a native path materialize the full result eagerly by default (not
bounded-memory). Pass native_only=True to require a native stream and raise
ImproperConfigurationError when the adapter has no native path.
streaming rows#from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
with spec.provide_session(config) as session:
session.execute("create table readings (id integer primary key, value integer)")
session.execute_many("insert into readings (value) values (?)", [(i,) for i in range(250)])
total = 0
with session.select_stream("select value from readings order by id", chunk_size=100) as stream:
for row in stream:
total += row["value"]
Adapter capability is discoverable via Config.supports_native_row_streaming.
Native paths: psycopg and CockroachDB-psycopg (server-side named cursors),
asyncpg and CockroachDB-asyncpg (cursors inside a stream-owned transaction),
pymysql/aiomysql/asyncmy (SSCursor), mysql-connector (unbuffered cursors),
sqlite/aiosqlite and oracledb (chunked fetchmany), psqlpy (server-side
cursor with array_size), and BigQuery (page-wise result iteration).
ADBC, DuckDB, mssql-python, and Spanner are eager-fallback only for row
streaming. arrow-odbc row streaming also materializes dict rows eagerly, but
select_to_arrow(..., return_format="reader" | "batches") uses the native
arrow_odbc RecordBatchReader path.
Lifetime and transaction rules:
Close the stream (or exhaust it) before issuing other statements on the same connection.
close()mid-iteration releases the cursor and is idempotent.psycopg, asyncpg, and psqlpy streams open their own transaction (a savepoint when one is already active) and commit it on close, so they stream correctly even on autocommit connections.
MySQL unbuffered cursors drain remaining rows when closed mid-iteration.
oracledb streaming returns raw driver values for LOB columns.
BigQuery
page_sizeis advisory; pages may exceed the requested chunk size.An exception raised during iteration closes the stream and propagates; the connection remains usable afterwards (issue a rollback first on PostgreSQL drivers, whose transaction is aborted by the failed statement).
Statement Stacks#
Statement stacks bundle multiple SQL statements plus parameter sets. Drivers that support native pipelines or batch execution can send the stack in a single round trip, while others execute each statement sequentially.
statement stack#from sqlspec import SQLResult, SQLSpec, StatementStack
from sqlspec.adapters.sqlite import SqliteConfig
spec = SQLSpec()
config = spec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
stack = (
StatementStack()
.push_execute("create table teams (id integer primary key, name text)")
.push_execute_many("insert into teams (name) values (:name)", [{"name": "Litestar"}, {"name": "SQLSpec"}])
.push_execute("select id, name from teams order by id")
)
with spec.provide_session(config) as session:
results = session.execute_stack(stack)
final_result = results[-1].result
assert isinstance(final_result, SQLResult)
rows = final_result.all()
Multi-Statement Scripts#
When initializing schemas or running migration scripts containing multiple SQL statements separated by semicolons, use execute_script():
with config.provide_session() as session:
session.execute_script("""
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE teams (id INTEGER PRIMARY KEY, title TEXT);
INSERT INTO teams (title) VALUES ('Core');
""")
Exporting Results to Storage#
Drivers can stream query results directly into local files or cloud object storage (AWS S3, Google Cloud Storage, Azure Blob Storage) in Parquet or CSV format without materializing the full dataset in memory:
with config.provide_session() as session:
# Export query results directly to Parquet on S3 or local disk
exported_bytes = session.select_to_storage(
"SELECT id, name, email, created_at FROM users WHERE active = :active",
destination="s3://my-analytics-bucket/exports/users.parquet",
file_format="parquet",
active=True,
)
Unified Exception Hierarchy#
SQLSpec translates underlying database driver errors into a consistent exception hierarchy:
SQLSpecError: Base class for all SQLSpec exceptions.DatabaseConnectionError: Connection establishment, pooling, or network failures.OperationalError: Database operational failures (lock timeouts, server disconnection).ProgrammingError: SQL syntax errors or invalid database object names.IntegrityError: Relational constraint violations. Subclasses include: -UniqueViolationError: Unique constraint or primary key violation. -ForeignKeyViolationError: Foreign key reference violation. -NotNullViolationError: NOT NULL constraint violation. -CheckViolationError: Check constraint violation.
Driver Configuration Examples#
asyncpg config#from sqlspec.adapters.asyncpg import AsyncpgConfig
config = AsyncpgConfig(
connection_config={"dsn": "postgresql://user:pass@localhost:5432/app", "min_size": 1, "max_size": 5}
)
cockroach + psycopg#from sqlspec.adapters.cockroach_psycopg import CockroachPsycopgSyncConfig
config = CockroachPsycopgSyncConfig(connection_config={"dsn": "postgresql://user:pass@localhost:26257/defaultdb"})
mysql connector config#from sqlspec.adapters.mysqlconnector import MysqlConnectorSyncConfig
config = MysqlConnectorSyncConfig(
connection_config={"host": "localhost", "user": "app", "password": "secret", "database": "app_db"}
)