Execution
Validate, inspect SQL, and execute Python dataset queries.
create_dataset_client is the entry point for running semantic queries. It plans each query, hands the compiled
statement to an executor, and returns rows keyed by column name.
from hypequery.datasets import create_dataset_client
from hypequery.execution import ClickHouseConnection, create_clickhouse_executor
# Reads CLICKHOUSE_URL, or CLICKHOUSE_HOST, _PORT, _DATABASE, _USERNAME, _PASSWORD and
# _SECURE; or pass host=..., username=..., password=..., database=... explicitly.
executor = create_clickhouse_executor(ClickHouseConnection.from_env())
analytics = create_dataset_client(executor=executor)The client does not own the executor, so call executor.close() when your application shuts down. The factory
connects as soon as it is called, so create the executor in your app's startup rather than at import time if the app
must start while ClickHouse is down. In a FastAPI app, close it from the app's lifespan. For async code, use
create_async_dataset_client with an async executor and await analytics.execute(...).
In Django and Flask, let the Django or Flask integration own the client: those frameworks have no shutdown hook, and the integrations create, close and tenant-bind it for you.
Connecting
ClickHouseConnection(host="ch.internal", database="analytics", username="reader", password=password)
ClickHouseConnection.from_url("clickhouses://reader:pw@ch.example.com/analytics")
ClickHouseConnection.from_env()| URL | Connects with |
|---|---|
clickhouse://, http:// | Plain HTTP (port 8123 by default) |
clickhouses://, https:// | TLS (port 8443 by default) |
clickhousedb://, clickhouse+http:// | SQLAlchemy URLs for the HTTP interface; ?secure=true or ?protocol=https for TLS |
The path is the database (default when omitted), and the user and password are percent-decoded. Native-protocol URLs
such as clickhouse+native:// are refused: hypequery sends values as server-side parameters, which only the HTTP
interface carries. No error message repeats the URL.
from_env() reads CLICKHOUSE_URL when it is set, and CLICKHOUSE_HOST, _PORT, _DATABASE, _USERNAME,
_PASSWORD and _SECURE otherwise. Setting both is an error. To reuse a SQLAlchemy engine's server, see
SQLAlchemy.
Client methods
| Method | Use it for |
|---|---|
analytics.validate(target, query, context=...) | Check a query without executing it |
analytics.to_sql(target, query, context=...) | Inspect a redacted debug statement |
analytics.execute(target, query, context=...) | Plan, execute, and return rows |
analytics.for_tenant(scope) | Get a client bound to one tenant |
A target is a Dataset or the name of a registered dataset. A query is a DatasetQuery or a plain mapping,
validated strictly with unknown keys rejected. A mapping uses the Python field names, so write order_by, not the
HTTP body's orderBy.
DatasetQuery(timezone="Europe/Berlin") sets the zone for time buckets and offset-free time-key bounds for one query;
create_dataset_client(timezone=...) sets the default, UTC unless given.
Validate
from hypequery.datasets import DatasetQuery, filter, order
validation = analytics.validate(
orders,
DatasetQuery(dimensions=("country",), filters=(filter.eq("status", "completed"),)),
)
if not validation.valid:
print(validation.errors)Inspect SQL
sql = analytics.to_sql(
orders,
DatasetQuery(
dimensions=("country",),
measures=("revenue",),
filters=(filter.eq("status", "completed"),),
order_by=(order.desc("revenue"),),
limit=10,
),
)The statement has placeholders instead of values and cannot be executed.
Results
execute returns a result with data (rows keyed by column name), columns, and meta with query_id,
row_count, timing_ms, and cache.
meta.pagination is None unless you pass paginate=True:
page = analytics.execute(orders, DatasetQuery(dimensions=("country",), limit=50, offset=100), paginate=True)
page.meta.pagination # Pagination(limit=50, offset=100, has_more=True)The client fetches one extra row to compute has_more and drops it from data. There is no total row count.
Value types differ from the HTTP wire, where every measure is a string:
- integer counts come back as
int; - floating-point results come back as
float; Decimalvalues come back asstr, to keep their precision;- timestamps come back as ISO 8601 strings.
For analysis, result.to_pandas(), to_polars() and to_arrow() restore real types from the dataset definition. See
pandas, Polars and Arrow.