How to connect an engine to your warehouse¶
An Engine owns one ADBC connection
pool and the dialect for a warehouse. You build it once with
create_engine(), then run queries through it. This guide
covers the two ways to use an engine, pool sizing, lifecycle, and querying
several warehouses side by side.
Two ways to use an engine¶
There are two patterns, and you can mix them in the same application.
The direct engine pattern keeps a reference to the engine and calls it
yourself. It mirrors SQLAlchemy: create_engine(...) hands you an engine, and
engine.execute(query) runs a query through its pool.
from adbc_poolhouse import SnowflakeConfig
from semolina import (
SemanticView,
Metric,
Dimension,
create_engine,
)
class Sales(SemanticView, view="sales"):
revenue = Metric()
country = Dimension()
engine = create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_analytics",
password="...",
database="analytics",
warehouse="compute_wh",
)
)
query = (
Sales.query()
.metrics(Sales.revenue)
.dimensions(Sales.country)
)
cursor = engine.execute(query)
for row in cursor.fetchall_rows():
print(row.country, row.revenue)
The named registry pattern registers an engine under a name and lets the
query resolve it. It mirrors Django’s database aliases: register once at startup,
then .execute() finds the engine for you. A query with no .using() clause
resolves the "default" engine.
from semolina import register, create_engine
register("default", create_engine("default"))
# No .using() -> resolves the "default" engine
cursor = (
Sales.query()
.metrics(Sales.revenue)
.dimensions(Sales.country)
.execute()
)
for row in cursor.fetchall_rows():
print(row.country, row.revenue)
The registry pattern is the better fit for web applications, where the engine is created once at startup and endpoints query without passing it around. The direct pattern suits scripts and notebooks where you already hold the engine.
Build an engine from a config object or a connection name¶
create_engine() accepts either an adbc-poolhouse config
object or the name of a .semolina.toml connection section. The dialect is
derived from the config type, so you never select a backend by hand.
Pass a config object when credentials come from a vault, a secrets manager, or your own code:
from adbc_poolhouse import SnowflakeConfig
from semolina import create_engine
engine = create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_analytics",
password="...",
database="analytics",
warehouse="compute_wh",
)
)
from adbc_poolhouse import DatabricksConfig
from semolina import create_engine
engine = create_engine(
DatabricksConfig(
host="workspace.cloud.databricks.com",
http_path="/sql/1.0/warehouses/abc123",
token="dapi...",
)
)
from adbc_poolhouse import DuckDBConfig
from semolina import create_engine
engine = create_engine(
DuckDBConfig(database="/path/to/warehouse.db")
)
Pass a connection name to read settings from .semolina.toml instead. The name
maps to a [connections.<name>] section:
[connections.default]
type = "snowflake"
account = "xy12345.us-east-1"
user = "svc_analytics"
password = "..."
database = "analytics"
warehouse = "compute_wh"
from semolina import create_engine
engine = create_engine(
"default"
) # reads [connections.default]
create_engine() with no argument is the same as create_engine("default").
Point at a different file with create_engine("default", config_path="config/warehouse.toml").
Size the pool¶
Pool sizing lives on the config object. The config classes carry pool_size
(steady-state connections), max_overflow (burst capacity above pool_size),
timeout, and recycle. The defaults are 5 and 3, so up to 8 concurrent
connections:
from adbc_poolhouse import SnowflakeConfig
from semolina import create_engine
engine = create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_analytics",
password="...",
database="analytics",
warehouse="compute_wh",
pool_size=10,
max_overflow=5,
timeout=30,
recycle=1800,
)
)
Set the same fields in a .semolina.toml section to size a pool you build by
name:
[connections.default]
type = "snowflake"
account = "xy12345.us-east-1"
user = "svc_analytics"
password = "..."
database = "analytics"
warehouse = "compute_wh"
pool_size = 10
max_overflow = 5
recycle = 1800
The pool parameters control connection behaviour:
Parameter |
Default |
Description |
|---|---|---|
|
|
Number of connections kept open and reused |
|
|
Extra connections allowed above |
|
|
Seconds to wait for a connection before raising an error |
|
|
Seconds before a connection is replaced with a fresh one |
Tip
Start with pool_size matching your expected concurrent query count (e.g. web
server worker count), and set max_overflow to 50–100% of pool_size for
traffic spikes. A recycle of 1800 seconds (30 minutes) prevents stale connections
from accumulating during low-traffic periods.
Note
DuckDB defaults to pool_size=1. In-memory databases (:memory:) are
isolated per connection, so pool_size > 1 with :memory: raises a
ValidationError. Use a file-backed database path for multiple concurrent
connections.
Open a raw connection¶
When you need to run something the query builder does not cover, check a
connection out of the engine’s pool with engine.connect(). It is a context
manager, so the connection returns to the pool on exit:
with engine.connect() as conn:
cur = conn.cursor()
cur.execute("SHOW VIEWS")
rows = cur.fetchall()
Manage the engine lifecycle¶
Create the engine at application startup and close it at shutdown. close_pool()
from adbc-poolhouse releases both the pool and the underlying ADBC source
connection. The engine holds its pool as engine._pool:
from adbc_poolhouse import SnowflakeConfig, close_pool
from semolina import register, unregister, create_engine
# Startup
engine = create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_analytics",
password="...",
database="analytics",
warehouse="compute_wh",
pool_size=10,
)
)
register("default", engine)
# ... application runs ...
# Shutdown
unregister("default")
close_pool(engine._pool)
unregister() removes the engine from the registry so no new
queries resolve it. close_pool() then disposes the pool and closes the ADBC
driver connection.
Warning
Call close_pool(engine._pool) rather than engine._pool.dispose().
close_pool() also closes the underlying ADBC source connection, preventing
resource leaks.
Look up a registered engine¶
get_engine() returns the engine registered under a name, so you
can reach it without keeping your own reference:
from semolina import get_engine
engine = get_engine("default")
Call it with no argument (or None) to get the "default" engine – the same
lookup .execute() performs when a query has no .using() clause:
engine = get_engine() # the "default" engine
To close an engine at shutdown without tracking your own reference, look it up, unregister it, then close its pool:
from adbc_poolhouse import close_pool
from semolina import get_engine, unregister
engine = get_engine("reports")
unregister("reports")
close_pool(engine._pool)
If no engine is registered under the name, get_engine raises ValueError
listing the names that are available.
Query several warehouses with .using()¶
Register engines under different names to query multiple warehouses, or to use different credentials for different workloads:
from adbc_poolhouse import SnowflakeConfig
from semolina import register, create_engine
# Production engine -- large warehouse, for dashboard queries
register(
"default",
create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_dashboard",
password="...",
database="analytics",
warehouse="large_wh",
pool_size=20,
max_overflow=10,
)
),
)
# Reporting engine -- small warehouse, for scheduled reports
register(
"reports",
create_engine(
SnowflakeConfig(
account="xy12345.us-east-1",
user="svc_reports",
password="...",
database="analytics",
warehouse="small_wh",
pool_size=3,
)
),
)
Use .using() on a query to pick which engine to run against:
from semolina import SemanticView, Metric, Dimension
class Sales(SemanticView, view="sales"):
revenue = Metric()
country = Dimension()
# Uses the "default" engine (implicit)
cursor = (
Sales.query()
.metrics(Sales.revenue)
.dimensions(Sales.country)
.execute()
)
# Uses the "reports" engine (explicit)
cursor = (
Sales.query()
.metrics(Sales.revenue)
.dimensions(Sales.country)
.using("reports")
.execute()
)
Engine resolution is lazy – it happens at .execute() time, not when
.using() is called. You can build queries before any engine is registered.
To drive multiple named engines from one TOML file, define a section per connection and build each by name:
[connections.default]
type = "snowflake"
account = "xy12345.us-east-1"
user = "svc_dashboard"
password = "..."
database = "analytics"
warehouse = "large_wh"
[connections.reports]
type = "snowflake"
account = "xy12345.us-east-1"
user = "svc_reports"
password = "..."
database = "analytics"
warehouse = "small_wh"
from semolina import register, create_engine
register("default", create_engine("default"))
register("reports", create_engine("reports"))
Close every engine at shutdown¶
When running multiple engines, close each one individually:
from adbc_poolhouse import close_pool
from semolina import get_engine, unregister
for name in ("default", "reports"):
engine = get_engine(name)
unregister(name)
close_pool(engine._pool)
get_engine() lets you reach each engine by name at shutdown, so
you do not have to thread engine references through your application.
See also¶
How to choose and configure a backend – connection patterns and backend selection
How to connect to Snowflake – Snowflake TOML fields and credentials
How to connect to Databricks – Databricks TOML fields and credentials
How to connect to DuckDB – DuckDB TOML fields and connection details
How to use Semolina in a web API – engine lifecycle in a FastAPI application
Configuration file reference – the
.semolina.tomlfile format