measure.approx_count_distinct(field)estimates a distinct count with ClickHouseuniq(uniqIfwith measure filters), far cheaper than the exactcount_distinct(uniqExact) on large columns. Catalogs and semantic contracts mark it, and derived measures that use it,approximate: true, as TypeScript does; it is duplicate-insensitive throughbelongs_to. Publishing it is refused until deployment contract 3, as in TypeScript.Exported client contracts keep
isNullandisNotNullon boolean filters that allow them (RFC 0023), instead of dropping them.Documentation for the ported ClickHouse features: null checks, aggregate states, row resolution, segments, window and shift measures, in the Python datasets pages and the package README. A shared execution corpus (
specs/datasets/time-measures-corpus-v1.json) now checks Python's window and shift results row for row against TypeScript.measure.shiftcan wrap a window, a derived measure or another shift, as in TypeScript: a prior-year year-to-date, a shifted average order value, orshift(shift(x)). The wrapped measure is recalculated over the earlier interval, and shifted formulas divide shifted aggregates. Derived measures over derived measures that read windows or shifts are now planned too, instead of refused. Definitions reject dependency cycles through shifts and formulas, and catalogs report the grains both the shift and its input support. A shifted formula has no single aggregation, so its catalog and semantic contract entries omitaggregationandfield, hashing the same as TypeScript. A query is refused asinput-invalidwhen nested shifts would need more than 64 evaluation contexts or 1024 evaluated measures.client.stream(target, query)andclient.stream_arrow(target, query)stream a dataset query's result instead of returning it whole, so memory stays flat however many rows there are.streamyields batches of the rowsexecutereturns;stream_arrowyieldspyarrow.RecordBatches with ClickHouse's native column types (decimal128,uint64), withread_all()andto_reader()for DuckDB, polars and pandas (hypequery[arrow]). Both run the queryexecutewould, under the same settings, tenant scope, deadline and cancellation, without the cache or pagination. Use them as context managers (async withon the async client): closing a stream early stops the query, withKILL QUERYwhere a control connection is configured. Every ClickHouse and chDB executor streams; a result that grows pastmax_result_rowsormax_result_bytesfails astoo-largerather than ending short. Custom executors stream by implementing the newStreamingQueryExecutorprotocol.Streams have ceilings of their own.
DatasetLimits(max_stream_rows=...)lets a dataset allow streams more rows than the pages and agentsmax_result_sizebounds; a stream with nolimitruns under that ceiling as itsLIMITand ends there. A stream's memory stays flat, so its byte limit isstream_max_result_bytes, a newcreate_dataset_clientoption of at most 1 TiB. Without it, a stream keeps amax_result_bytesthe client's settings set and runs under 1 TiB in place of the default.max_result_rowsandmax_execution_timestill bound it, and a request context can still tighten all three.hypequery demo: a sample semantic layer on embedded ClickHouse, served to an AI agent over MCP stdio with no server, configuration or network. With only uv installed,uvx --from "hypequery[chdb,mcp]" hypequery demoseeds a two-tenant shop in a temporary chDB database (deleted on exit) and serves itsorders,eventsanddaily_activitydatasets.--self-testprints the answers in the terminal, each beside the read a naive query gets: plain reads of a ReplacingMergeTree againstresolved_rows.replacing(),count_distinctagainstapprox_count_distinct,merge("uniq")over an AggregatingMergeTree against summed daily counts, andmeasure.shiftandmeasure.trailingresults.--print-configprints anmcpServersentry and--tenantpicks the tenant the agent is signed in as.Float measures serialize about 65 times faster, with byte-identical output. Python serve's NDJSON streams spent nearly all their time formatting float measures as strings: streaming 1M rows took 44 s and now takes about 4 s, on a par with TypeScript serve. JSON pages, MCP results and canonical protocol values use the same serializer and get the same speedup.
hypequery inittelemetry now reports how far setup got (stage_reached: started, connection_tested, framework_selected, confirmed or completed) and whether connecting to ClickHouse worked (connection_result: ok, failed or skipped), as the TypeScript CLI does. Both are fixed enums: never hosts, table names or anything typed at a prompt.measure.merge(state_function, field)reads anAggregateFunctioncolumn of an AggregatingMergeTree table (or the materialized view feeding one) by merging its states:measure.merge("uniq", "users")isuniqMerge(users), so a week of daily states merges into one distinct count instead of summing daily counts (RFC 0022). The state function must be one of RFC 0022's list; merge measures take no filters orsqland are not merged through a relationship. Estimating states (uniq,uniqCombined, ...) are markedapproximate, and the deployment contract carriesstateFunction, matching TypeScript (shared fixturespecs/datasets/merge-authoring-v1.json).A native MCP server:
hypequery mcp [module:api]serves an API's authorized client contract to AI agents over stdio, with the same four tools (list_datasets,get_dataset_schema,query_metric,query_dataset), input schemas,dataset_guideprompt andMCP_*errors as@hypequery/mcp. Calls run throughApi.handlein-process, so the API's authentication, policy and tenant apply;--client-contract-urlbridges a deployed API instead,--self-testchecks the contract and--print-configprints anmcpServersentry. Node is no longer needed. Install themcpextra.hypequery.mcpexposes the pieces:ClientContractExecutor,ApiContractBackend,HttpContractBackend,create_mcp_serverandserve_stdio.The standalone
hypequery initapp now publishes a client contract (client_profile=ClientQueryProfile()andGET /client-contract), sohypequery mcp app:apiworks on a fresh project.Null-check filters (RFC 0023):
filter.is_null(field)andfilter.is_not_null(field)renderIS NULL/IS NOT NULLwith nothing bound. Measures may use them in fixedfilters; callers may use them only on filters whoseFilterDefinition(operators=...)lists them, so the default operator list, and existing deployment identities, are unchanged. A null check carries novalue(not evennull); served requests and the cache and deployment wire omit it. ExportedSEMANTIC_NULL_CHECK_OPERATORS.Resolved-row sources (RFC 0021):
dataset(..., row_resolution=...)reads a ReplacingMergeTree or CollapsingMergeTree source withFINAL, so superseded row versions never double count.resolved_rows.replacing(deleted_column=...)also keeps only rows not soft-deleted, andresolved_rows.collapsing(sign_column=...)/.versioned_collapsing(...)only surviving positive rows;resolved_rows.final()isFINALalone. Relationship targets are resolved inside their join, and relationship key checks count current rows. Every statement that reads a resolved source pinsoptimize_move_to_prewhere_if_final = 0, so no filter can match an older version. Catalogs, semantic contracts and deployment contracts carryrowResolution, as TypeScript does.Joins and relationship key checks over a resolved (
FINAL) target qualify its current-row column, so a deleted or sign column named like an internal alias (_hq_match,__hq_rows,__hq_keys) no longer reads that alias.Segments (RFC 0015):
dataset(segments={"paid": segment(filter.eq("status", "paid"))})declares named row filters that queries select by name,DatasetQuery(segments=("paid",))or"segments": ["paid"]in a served request. A segment may use any of the dataset's own dimensions, including ones not exposed as filters, but not a relationship or the tenant column, and may use null checks. Catalogs and contracts list each segment's label and description only. Publishing is refused until deployment contract 3, as in TypeScript.Dataset endpoints can stream their results. With
EndpointPolicy(stream_max_rows=...)(at most the dataset'smax_stream_rows, elsemax_result_size), a request that asks withAccept: application/x-ndjsongets row batches as NDJSON frames (start,rows…, thenendwithrowCount, or anerrorframe if the query fails after the response began), andAccept: application/vnd.apache.arrow.streamgets an Arrow IPC stream, frame for frame as TypeScript serve sends them. Streams keep the endpoint's auth, tenant scope and validation, are never cached, and stop the query when the caller goes. They needto_asgi_app; WSGI, Django views and Flask answer with the usual page. Under aProductionProfilea stream holds its slot, runs withintimeout_secondsand stops atmax_result_bytes: NDJSON with aPAYLOAD_TOO_LARGEframe, Arrow broken off before its end-of-stream marker.Serve sends a dimension integer beyond ±(2^53 − 1), such as a
UInt64id, as a string in JSON pages and NDJSON stream frames, so JavaScript clients (React hooks,fetch, MCP clients) no longer round it. Smaller integers stay numbers, and local Python client results keep nativeints.hypequery skill installwrites hypequery's agent skills (hypequery-datasets,hypequery-query) where Claude Code (.claude/skills/), Cursor and Codex (.agents/skills/) load them, with--agent,--scope project|user,--checkand--force. The same skills ship with the TypeScript CLI.Window and shift measures (RFC 0015):
measure.trailing("revenue", 7, "day"),measure.to_date("revenue", "month"),measure.cumulative("orders")andmeasure.shift("revenue", 1, "week")re-aggregate a base measure along the time axis, and derived measures can divide by them for period-over-period ratios. A query that selects one needs abygrain and exactly one bounded range on the time key. It plans one ClickHouse statement that fills every bucket and guards the series size, in the query timezone. Client endpoints do not advertise these measures, and publishing is refused until deployment contract 3, as in TypeScript.
hypequery (Python)
The Python SDK and CLI.
Latest: 0.1.0b4 · November 10, 2026 · PyPI · CHANGELOG.md
Datasets can run on embedded ClickHouse with the new
chdbextra:create_chdb_executor(path)(andcreate_async_chdb_executor) execute the same compiled, read-only, parameter-bound SQL in-process on chDB, with no server.pathis an on-disk directory or":memory:";database=sets where unqualified sources resolve. Results decode exactly as from a ClickHouse server.hypequery init --database chdbscaffolds a project on embedded ClickHouse, as the TypeScript CLI does: the standalone or FastAPI app opens a chDB database directory (--chdb-path, defaultanalytics.chdb) with no server or connection settings, and installs thechdbextra. An existing database is inspected like a server's; a new one gets the orders example and a seeding step.hypequery generate datasetsreads the same database from--chdb-path, the path init records inpyproject.toml, or a projectanalytics.chdbwhen no ClickHouse server is configured, and says plainly when another process such ashypequery devhas the database open.hypequery devserves a chDB project with reload, its default: the supervisor that checks the app before starting the worker now releases the app's database, since chDB holds a database directory for one process at a time (run_dev(reload=True)does the same; other chDB executors in the process stay open).The CLI now collects pseudonymous usage telemetry, on by default like the TypeScript CLI: one event per command with the command, flag names (never values), outcome, a duration bucket, and install context such as the Python version and installer. It never records code, SQL, names, paths or credentials, and never delays a command. Disable it with
hypequery telemetry disable,--no-telemetry,DO_NOT_TRACK=1orHYPEQUERY_TELEMETRY_DISABLED=1; setHYPEQUERY_TELEMETRY_DEBUG=1to print the event instead of sending it. Settings and the install ID are shared with the TypeScript CLI.New
hypequery telemetry [status|enable|disable]command and global--no-telemetryflag.create_api(client_profile=...)puts everyDatasetentry ofdatasets=in the client contract, so the shorthand can produce typed React hooks.A build export with no client endpoints now fails:
hypequery.serve.export_client_contract(api)raisesValueErrornaming the dataset and metric routes installed without a client profile, andhypequery export client-contractexits 1.hypequery export client-contract --factoryaccepts the installed API (anApi, itsto_asgi_apporto_wsgi_appapplication, or a zero-argument function returning one) and exports its build view, so the contract cannot drift from the served paths.Breaking: a
create_api(metrics={key: ...})key is the metric's name, as in TypeScript: its client-contract ID, React hook key and output alias. A metric created with a differentname=is refused.Portable semantic queries accept
havingandtimezone(RFC 0025).validate_protocol_semantic_queryacceptshavingon dataset queries (each condition names a selected measure, with a numeric comparison) andtimezonefrom RFC 0018's pinned registry, under extensions 1 and 2. NewProtocolHavingCondition,ProtocolHavingComparisonOperatorandProtocolHavingOperatorexports. Cache preimages refuse a non-emptyhavingor a non-UTCtimezone;"UTC"normalizes as if absent, so no existing preimage changes.The PyPI page now shows a short
README.pypi.md: what the package is, one define-and-query sample, a FastAPI mount, and links into the docs. The full guide stays inREADME.md. Package metadata gains theFramework :: FastAPIand library classifiers, discovery keywords (semantic layer, metrics, multi-tenant, FastAPI, Django, Flask, MCP, pandas, polars, Arrow), and a Documentation link to https://hypequery.com/docs/python.Null-check filters (RFC 0023):
filter.is_null(field)andfilter.is_not_null(field)renderIS NULL/IS NOT NULLwith nothing bound. Measures may use them in fixedfilters; callers may use them only on filters whoseFilterDefinition(operators=...)lists them, so the default operator list, and existing deployment identities, are unchanged. A null check carries novalue(not evennull); served requests and the cache and deployment wire omit it. ExportedSEMANTIC_NULL_CHECK_OPERATORS.Exported client contracts keep
isNullandisNotNullon boolean filters that allow them (RFC 0023), instead of dropping them.hypequery.protocol.build_client_contract_tool_schemas()compiles a signed client contract into thequery_datasetandquery_metricMCP tool input schemas, identical to@hypequery/datasets's, plus the manifest hash. It is the schema layer for a future Python MCP server; Python APIs still reach agents through@hypequery/mcp.Server-fault errors (
internal,unavailable) keep the reason they were raised with asCompiledQueryError.diagnostic, shown in tracebacks; the public message is unchanged. A relationship whose target dataset is missing from the client's registry now names the dataset and how to register it, instead of only "The query could not be executed."
hypequery initis guided in a terminal, as the TypeScript CLI is. It asks for whatever its flags leave unset: where to create the project; how to reach ClickHouse (the connection already in your environment, or a URL and credentials typed in, with the password hidden, used only to read the schema and never written to a file); which tables, when the database has more than ten; the tenant column, when one looks like a tenant id; and which app to scaffold, defaulting to FastAPI, Django or Flask when it finds one. A failed connection offers to try again, change the details, or create the offline example. It ends with the same choices as flags. Every flag answers its question, and--no-interactive, aCIenvironment variable, or input that is not a terminal skip the questions and keep the previous behavior.The
hypequeryCLI loads.env.localand.envfrom the directory it runs in, as the TypeScript CLI does. A variable already set wins over both files. The library and generated apps still read only the real environment;hypequery.cli.load_env_files(directory)applies the same rules in your own scripts, such as the scaffold README's seed step.When init can't reach ClickHouse, the message names
CLICKHOUSE_URLas well as the host settings and suggests--skip-connection, and the driver's own "Unexpected Http Driver Exception" log line no longer reaches the terminal.hypequery init --framework fastapiscaffolds a FastAPIapp.py: the bearer-token agents API mounted at/api/agents, and/analytics/<dataset>, a route of your own that refuses everyone until itscurrent_principaldependency reads your login. The project depends onfastapidirectly; hypequery adds no FastAPI extra.Declaring
filterson a dataset no longer makes its other dimensions unfilterable (RFC 0024), matching TypeScript. Callers may filter on every declared filter and every dimension not markedfilterable=False. A dimension's own filter takes the operators of the filters declared on its field, so declaringplan_filteronplanwithoperators=("eq",)also limitsplantoeq. Relationship filters reach every filterable, non-SQL dimension of abelongs_toorhas_onetarget. An explicit emptyfilters={}no longer disables filtering; mark a dimensionfilterable=Falseto keep it out. Catalogs, client contracts and deployment contracts are unchanged.Base dataset columns are always qualified (
__hq_base), not only when a relationship join is present. ClickHouse resolves an output alias before a column of the same name, so a dimensionstatusreading columncountrycaptured a filter or selection on the realstatuscolumn (silent wrong rows), and a measure named like its column (amount=measure.sum("amount")) broke any other measure or filter on that column.Under
tenants(...)with several tenants orall_tenants(), relationship joins also match the tenant keys, so a key reused by another tenant no longer joins that tenant's row.check_relationshipscounts (key, tenant) pairs under those scopes to match. Single-tenant SQL is unchanged.Formula
dividecasts the dividend to Float64. Decimal division kept the dividend's scale and truncated, soround(divide("revenue", null_if_zero("orders")), 2)could come out one cent low.hypequery init --tenant-columnscaffolds answer their README's first query: the development token reads the tenant inHYPEQUERY_DEV_TENANT(added to.env.exampleand the README), and endpoints over tenant-scoped datasets useEndpointPolicy(tenant="required"), so a principal without a tenant gets a clear 403 and its client contract leaves them out.hypequery init --tenant-columnrecords the column inschema.json, andhypequery generate datasetsreuses it unless given--tenant-columnor the new--no-tenant-column. Regenerating without repeating the flag used to drop everytenant_key. An unusable recorded value is refused before discovery rather than read as no tenant column.A strict client endpoint's
400 Invalid client query.now carriesdetails.issues[].path, the member or item that failed (for example["filters", 0, "value"]), as the README promised. Paths name contract members and indexes only; an unknown member is reported at the root, and no submitted value is echoed.Scaffolds discovered as a user other than
defaultno longer suggestCLICKHOUSE_USERNAME=defaultin.env.exampleand the README, which would quietly connect as a different, usually broader, user. They show a placeholder; the real name stays out of generated files, like the password.to_asgi_appandto_wsgi_applog one warning onhypequery.servewhen served below aroot_pathorSCRIPT_NAMEthat differs fromclient_mount_path, naming the setting. Contract paths still never follow a prefix the server or a proxy reports.Docs: a query mapping passed to
client.executeusesDatasetQuery's field names (order_by, not the HTTP wire'sorderBy); every scaffold README (standalone, Django and Flask, offline or from your schema) says timeouts needSELECT ON system.processesor a control user; a synchronous caller getsdeadline-exceededonly once ClickHouse stops the query.hypequery init(standalone): the generated API is now open for local development, like the TypeScript scaffold.app.pyauthenticates every request as one developer withfrom_context(..., same_origin=False), sohypequery devanswers curl without a token, andHYPEQUERY_DEV_TOKENis no longer required or listed in.env.example. The README andapp.pysay to add real authentication (jwt_strategy,from_contextwith the host's login, or a credential check) before deploying; aProductionProfilerefuses the open API andhypequery devstill warns when bound beyond loopback. Django and Flask scaffolds keep their bearer token for/api/agents.hypequery init --tenant-column(standalone): the README now explains why queries answer403 FORBIDDEN("requires runtime tenant scoping") and shows the one-line local fix,Principal(subject="developer", tenant_id="...");app.pycarries the same hint. Tenant-scoped datasets stay fail-closed by default.Django and Flask scaffolds:
principal_for_token's docstring now says it refuses every token whileHYPEQUERY_DEV_TOKENis unset.
hypequery init --framework standalone|django|flaskchooses the application written besidedatasets.py(decision 0023 step 6).standalone, the default, isapp.py: an API fromcreate_api, served as plain ASGI byhypequery devwith no web framework. It replaces thefastapichoice, which wrote the same app; to use FastAPI, mountappin it.--framework djangowrites a small runnable Django project (settings.py,urls.py,manage.py) andanalytics.py;--framework flaskwrites a Flaskapp.py. Both serve hypequery's endpoints natively (urls(api),blueprint(api)) twice: at/api/analyticsbehind the framework's own login (django_user,flask_session) with the same-origin check, and Django's CSRF middleware, for@hypequery/react; and at/api/agentsbehind a bearer token (HYPEQUERY_DEV_TOKENin development), for@hypequery/mcp. Each also has a view or route of its own on the per-request client. Until wired to a login, Flask's session API and route answer 401, and tenant-scoped datasets refuse signed-in Django users. Pyproject extras aredjangoorflask; the README covers secrets, hosts,jwt_strategyand moving the pieces into an existing Django project.--frameworkworks with--skip-connection: each framework gets the offline orders example, withdatasets.pyandseed.sql. The standalone example's dataset now lives indatasets.pytoo.hypequery initaccepts--tenant-column, likegenerate datasets, so a tenant-scoped project needs one command.hypequery initrecords--tables/--exclude-tablesinschema.json, andgenerate datasetsreuses that selection unless given--tables,--exclude-tablesor the new--all-tables. Regenerating no longer widens a project to every table in the database.The scaffold's
.env.example, README andapp.pymatch how discovery connected:CLICKHOUSE_URLwhen it was set, otherwise the host, port and TLS setting discovery used. Credentials stay placeholders."Possible tenant column" review notes name the
--tenant-columnflag that sets the key.generate datasetsandexport client-contractprint normalised paths.Add
labelanddescriptiontoDatasetanddataset(...). Client-contract exports and live discovery now advertise dataset, dimension and measure label/description as optional endpointannotations(RFC 0018 amendment), so MCP agents listing datasets see each dataset's own description. Whitespace runs are folded and blank text is omitted; text over 4096 UTF-8 bytes or containing control characters fails export. Annotations are excluded fromschemaHash, so editing a description does not triggerCLIENT_SCHEMA_CHANGED. Pair with@hypequery/mcp/@hypequery/clireleases that understand annotations.The Django and Flask integrations' process-wide client connects on its first query, not when it is created. Modules can hold
get_client()(Django) or the newhq.client_for(app)(Flask, no app context needed) at import time, and the app starts even with ClickHouse down. A closed client refuses to reconnect.Publish a dataset's
descriptionin deployment contracts and bundles, matching TypeScript. The contract has no dataset-level label, soDataset.labelis not published there. Descriptions follow the existing deployment text rules: blank, multi-line, control-character or over-4096-byte text fails the export.Docs:
django_user's page must set Django'scsrftokencookie (ensure_csrf_cookie, or a template using{% csrf_token %}), or Django answers its own HTML403before hypequery runs. Thedjango_userandurlsdocstrings and the--framework djangoscaffold's README now say so.hypequery.contrib.django.urlsanswers aHostoutsideALLOWED_HOSTSas the core answers one,400 Invalid host headeras plain text, instead of Django's HTMLDisallowedHostpage (the technical page underDEBUG). It carries the core'sx-request-id(and a valid caller id asx-correlation-id) and is still logged todjango.security.DisallowedHost. A host refused by middleware before the view runs, such asCommonMiddleware, still gets Django's own response.Docs: explain that per-endpoint
schemaHashvalues (not the documentcontentHash) match between the build export and the live discovery view, and how measure values differ between in-process results and hypequery's HTTP wire.When ClickHouse rejects the server's own connection, the error is now a server fault instead of the caller's. A wrong password or unknown user (
AUTHENTICATION_FAILED,UNKNOWN_USER,WRONG_PASSWORD,REQUIRED_PASSWORD,IP_ADDRESS_NOT_ALLOWED,USER_EXPIRED), a missing grant (ACCESS_DENIED,DATABASE_ACCESS_DENIED,NOT_ENOUGH_PRIVILEGES), a missing database (UNKNOWN_DATABASE) and a readonly-profile mismatch all raiseCompiledQueryError("unavailable"), whichhypequery.serveanswers with503 CLICKHOUSE_UNREACHABLEand the fixed message "The query executor is unavailable.". Before, they answered401 UNAUTHORIZED,403 FORBIDDENor404 NOT_FOUNDwith text that named ClickHouse. That sent browser and agent clients into re-login loops for a fault only an operator can fix. RFC 0010 keepsunauthenticatedandforbiddenfor the authentication layer.CompiledQueryErrorhas two new fields that never appear in the envelope:code(for exampleHQ_EXECUTOR_CREDENTIALS_REJECTED,HQ_EXECUTOR_ACCESS_DENIED,HQ_EXECUTOR_DATABASE_MISSINGorHQ_EXECUTOR_READONLY_MISCONFIGURED) anddiagnostic, a sentence for operators.hypequery.servelogs both to thehypequery.servelogger with the request id and query id. The readonly guidance that used to be the error message is now thediagnostic. CLI hints are keyed bycode.hypequery.contrib.flask.blueprintexempts an API that reads a header credential (bearer, API key, JWT) from Flask-WTF'sCSRFProtect, whetherCSRFProtectis set up before or after the blueprint is registered. A blueprint for an API on the app's own login (flask_session) is still checked, so the page sends the token asX-CSRFToken.ClickHouseConnection.from_url()readsclickhouse://,clickhouses://,http(s)://and SQLAlchemy HTTP-interface URLs (clickhousedb://,clickhouse+http://);from_env()(and the CLI) also readCLICKHOUSE_URL, which cannot be combined with the field variables. Native-protocol URLs are refused with an explanation, and no error repeats the URL.Dataset results carry
column_typesfrom the dataset definition and convert to typed DataFrames withto_pandas(),to_polars()andto_arrow()(newpandas,polarsandarrowextras). Timestamps become timezone-aware datetimes in the query timezone, counts nullable integers, numbers floats or, withdecimals="decimal", exact decimals.New
hypequery.contrib.django(djangoextra):HYPEQUERYsettings validated bymanage.py check, a per-process client closed at exit, and middleware giving each request a lazily built, tenant-boundrequest.analytics(in async views, an awaitable client whose tenant function runs off the event loop, so it may use the ORM).New
hypequery.contrib.flask(flaskextra): aHypequeryextension withapp.configkeys, atenant_loader, and per-request tenant-boundclient/async_client.New
hypequery.contrib.sqlalchemy:executor_from_engine()andconnection_from_engine()point hypequery at a SQLAlchemy engine's ClickHouse server with hypequery's own clients.New
create_async_from_sync_clickhouse_executor(): a thread-backed async executor that needs only theclickhouseextra.AsyncFromSyncClickHouseExecutor.close()now also closes its driver clients.Served query metadata omits
cachewhen the dataset client has no result cache, matching TypeScript. It previously reported{"hit": false}on every query, which read as a cache miss.hypequery initandhypequery generate datasetswrite a table's ClickHouseCOMMENTas the dataset'sdescription=and a column's as the dimension's, so client contracts and MCP agents describe datasets from the catalog instead of a generic fallback. Whitespace runs become one space and control characters are dropped to fit the deployment contract; a comment over 4096 bytes is shortened with a review warning.schema.jsonrecords the comments, only where set.Protocol deployment validation now accepts
mergemeasures with astateFunctionfrom the allowlist (RFC 0022), with the same rules as TypeScript: no filters orsql, and in contract 3 anapproximatemarker exactly for estimating states. This adds protocol representation only; Python code-first datasets do not yet author merge measures.Protocol validation accepts the
isNullandisNotNullcomparisons (RFC 0023), with the same rules as TypeScript: an expression comparison hasleftand noright, deployment filter operator lists and client contract filters may name them on any type, and a client query filter using one has novalue. A value on them is rejected. This adds protocol representation only; Python code-first datasets do not yet author null-check filters.Protocol deployment validation now preserves explicit
rowResolutionFINAL policies, including the engine-less{"kind": "final"}form, replacing soft-delete columns and collapsing sign columns, with the same strict grammar as TypeScript. This adds protocol representation only; Python code-first datasets do not yet author resolved sources.ServeRequest.scheme, used by the same-origin check's own-origin comparison, is filled in by every adapter (by Django and Flask themselves, soSECURE_PROXY_SSL_HEADERandProxyFixapply); a trusted proxy'sX-Forwarded-Protooverrides it.Every response from
hypequery.servenow carriesX-Content-Type-Options: nosniffandContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; a route that sets its own policy keeps it. TheInvalid host headerrefusal is sent withCache-Control: no-store.The development docs page (
development_docs=True) loads an exact Redoc release checked by Subresource Integrity, instead of whateverredoc@2currently is, under a Content-Security-Policy that allows only that script, and sendsReferrer-Policy: no-referrer.A request body must be UTF-8 JSON as
JSON.parsereads it:NaNandInfinity, strings holding a lone surrogate ("\ud800") and nesting deeper than 64 levels are refused with400 VALIDATION_ERROR. A lone surrogate in a filter value, and a deeply nested one, used to fail as a500; UTF-16 and UTF-32 bodies are no longer guessed at.The
djangoextra requiressqlparse>=0.6.0and theflaskextrajinja2>=3.1.6, the first releases free of published advisories; Django and Flask themselves admit older ones.The sdist no longer picks up local coverage data, reports,
.envfiles or SQLite databases from the checkout it is built in, and the release artifact audit refuses an archive that contains them.hypequery.serveno longer needs FastAPI (decision 0023 step 3).create_api(auth=...)returns a framework-neutralApi, modelled on TypeScript'screateAPI:auth=(renamed fromauthenticate=),credentials=,tenant=(renamed fromresolve_tenant=),security=,production=,limits=,events=,base_path=(replacingprefix=),max_body_bytes=,development_docs=,openapi=andclient_mount_path=.datasets={"name": dataset}servesPOST /datasets/<name>/query,metrics={"name": create_metric_endpoint(...)}servesPOST /metrics/<name>, and with eitherGET /discoveryis served, authenticated by default.api.add_route(method, path, handler)serves your own routes;await api.handle(ServeRequest(...))serves a request with no server.New
to_asgi_app(api, lifespan=...): a pure ASGI application with no Starlette, for uvicorn and any ASGI server, or to mount in FastAPI, Starlette or Litestar. It streams the request body lazily under the body policy, cancels a query whose caller disconnects, runs the optional lifespan context and closes websockets. With a production profile, writing the response is inside the request's admission slot and deadline.Development OpenAPI (3.1) and docs come from the routes' own Pydantic schemas (
openapi_document(api),OpenApiInfo):GET /openapi.jsonand a Redoc page atGET /docs, as in TypeScript./redocis gone.start_server(api),run_dev(api)andhypequery devrun an API or itsto_asgi_appapplication on uvicorn. Thefastapiextra is replaced byserver, which installs only uvicorn;hypequery.serveitself needs no extra (anyio is now a base dependency).Removed, before the first PyPI release:
ServeRouter,create_router,create_app,install_http_security,request_id(request)(readcall.state.request_id), theRateLimitFastAPI dependency (passlimits=instead) and therouters=arguments ofexport_client_contractandadd_client_contract_endpoint.export_client_contracttakes the API or its ASGI application.ServeErroris no longer a FastAPIHTTPException.Routing matches the path as sent, as TypeScript does: a trailing slash is
404 NOT_FOUNDinstead of FastAPI's307redirect, and a percent-encoded path no longer matches its decoded route. Every other answer is byte-identical to the FastAPI layer's, header order aside.hypequery.serve.from_context(extract)authenticates with the host app's own login, as TypeScript'sfromContextdoes:create_api(auth=from_context(extract))handsextracttheServeRequest, which reads the session fromrequest.rawand returns aPrincipalor None (401). A same-origin React page then calls the endpoints with its session cookie and no token. Withsame_origin=True, the default, the core refusesPOST,PUT,PATCHandDELETEwith403 FORBIDDENunlessSec-Fetch-Siteissame-originornone,Originis the request's own origin (its scheme andHost, as Django's CSRF check compares them), orOriginis in the API'sHttpSecurity.cors.origins, under every adapter. A browser without Fetch Metadata (Safari before 16.4) calling its own site needs noCorsPolicyentry. A sync extractor runs on the request's own thread under WSGI, Django and Flask (ServeRequest.run_on_request_thread).hypequery.contrib.django.django_user(to_principal)andhypequery.contrib.flask.flask_session(to_principal)build these strategies.django_userreadsrequest.user, through the async ORM in async views;flask_sessionreads Flask-Login'scurrent_userwhen the app uses Flask-Login, elsesession. Django'surls(api)leaves Django's CSRF middleware active for them, so the page sendsX-CSRFToken; header credentials (bearer, API key, JWT) stay CSRF-exempt.Production now accepts
from_contextand refusesfrom_context(..., same_origin=False), replacing the flat cookie ban:ProductionProfile(cookie_auth=...)is removed, andbearer_token/api_keyrefuse thecookieheader in any profile.The Django and Flask examples serve their API behind their own login for a same-origin React page, and behind a bearer token at
/api/agentsfor the MCP bridge.jwt_strategyandanalytics_token_issuerinhypequery.serve, with a newjwtextra (PyJWT and cryptography).jwt_strategy(secret=... | jwks_url=..., issuer=, audience=)is an authenticator forcreate_apithat verifies JWT bearer tokens and mapssub,org_id,rolesandscope/scopesto thePrincipal, as TypeScript'screateJwtStrategydoes.analytics_token_issuer(secret)mints the short-lived tokenscreateAnalyticsTokenIssuermints. Tokens from either language verify in the other; shared fixtures (specs/serve-http/fixtures/jwt-v1) pin which tokens both accept and refuse. Key sets are fetched off the event loop with a timeout and cached; an unreachable key set answers 503. Python is stricter than TypeScript in a few places: it refuses tokens withoutsub, HMAC secrets shorter than the hash, andhttpkey-set URLs outside localhost.Django's
urls(api)now checks the request'sHostagainstALLOWED_HOSTSitself, so an API without anHttpSecurityis protected even in a project withoutCommonMiddleware.ServeRequest.is_disconnectedis replaced bywait_disconnected, an awaitable that returns once the caller has gone. The core no longer polls for disconnection every 50 ms per running query.The default tenant resolver runs inline instead of on a worker thread for every authenticated request.
A WSGI, Django or Flask request arriving after interpreter exit has stopped the serving loop is refused instead of starting a loop that is never closed.
Removed the re-export modules
hypequery.serve.security,hypequery.serve.rate_limit,hypequery.serve.request_idsandhypequery.serve.errors; import those names fromhypequery.serve.Django and Flask serve hypequery's HTTP endpoints natively, for
@hypequery/reactand@hypequery/mcp, with no FastAPI and no second app (decision 0023 step 4).hypequery.contrib.django.urls(api)returns URL patterns toinclude()below any prefix: the view is async under ASGI and sync under WSGI,ServeRequest.rawis theHttpRequest, and the body is read from the request's stream, bounded by the API's body policy rather thanDATA_UPLOAD_MAX_MEMORY_SIZE. An API whose credential is a header (bearer, API key, JWT) is CSRF-exempt; one on the site's own login (django_user) stays under Django's CSRF middleware, so the page sendsX-CSRFToken.hypequery.contrib.flask.blueprint(api)returns a blueprint to register below anyurl_prefix;rawis Flask's request, and host code runs in the request's Flask context. Client contracts and discovery name the paths a request was routed under, unlessclient_mount_pathis set.hypequery.serve.to_wsgi_app(api)serves an API to gunicorn, waitress or any WSGI framework. Sync hosts run the API on one event loop per process, on its own thread, while the request's thread waits and reads the body for it, so no loop is created per request and threaded servers serve requests concurrently. The loop restarts in each forked worker and stops at exit.Api.handleandApi.servetakemounted_at=, the prefix a host framework's router served the request under, for adapters mounted inside one.HttpSecurity(allowed_hosts=...)now matches bracketed IPv6 literals ([::1]allows[::1]:8000and[0:0::1]), compares names case-insensitively and ignores a trailing dot. An allowed host that is not a valid host, such as[nope], is refused when the profile is built.With CORS configured, every response carries
Vary: Origin, including responses to requests without anOrigin, and preflight answers also vary onAccess-Control-Request-Method,-Headersand-Private-Network. This keeps shared caches from serving one origin's answer to another.Avoid terminating the process holding a dataset regeneration lock on Windows.
Give the ClickHouse installation hint only when that driver is the missing app dependency, including when the executor factory reports it.
Redact application import errors in development reload workers as well as initial startup.
Add authenticated
add_client_contract_endpointdiscovery for installed strict client endpoints. Apply each target's role/scope/tenant policy before projection, sign the caller's authorized view, and return no-store responses that vary by credential transport. Keep metric-only/empty views valid, enforce a bounded response budget without truncation, and expose no tenant values or physical SQL.Add
hypequery export client-contract --factory module:factoryfor offline canonical artifacts, including JSON-only stdout, bounded input/output handling, read-only drift checks and explicit atomic replacement. Validate both signed contracts and prepared identities, redact factory chatter/import failures, and preserve concurrent edits and file permissions without importing serving/driver extras. The explicitly named project factory should use definition-only SDK APIs.Add definition-only
ClientEndpointDefinition,ClientQueryProfile, andexport_client_contractAPIs for deterministic frontend/MCP build artifacts. Export logical dimensions, safe related measures, target-authorized filters, metric output aliases, effective definition limits and ordered base defaults without importing FastAPI, connecting to ClickHouse, or exposing physical SQL.Add portable client-contract v1 models, bounded JSON decoding, canonical identities, and strict HTTP/MCP query validation for generated React and MCP integrations. Validate the same shapes, safe failures and query profile as TypeScript through a required shared conformance family.
Add a runnable Python → generated React → HTTP MCP tutorial with tenant-scoped ClickHouse serving, decimal/null seed data, per-login browser cache isolation and installed-artifact preparation.
Add opt-in
client_profilebindings for Python dataset/metric endpoints and router/application client-contract export using resolved FastAPI paths. Enforce the advertised strict query profile, clamp against actual policy, client and production ceilings, and checkX-Hypequery-Client-Schemaonly after authorization. Reject stale pins with a safe HTTP 409. Legacy endpoints retain their input behavior and are omitted from client exports. Add trustedclient_mount_pathconfiguration and the matching default CORS allow-header.Disable automatic ClickHouse driver sessions for owned query/control clients so overlapping HTTP and worker queries can share an executor without session-lock failures.
Add
ClickHouseConnection.from_env(), readingCLICKHOUSE_HOST,CLICKHOUSE_PORT,CLICKHOUSE_DATABASE,CLICKHOUSE_USERNAME,CLICKHOUSE_PASSWORDandCLICKHOUSE_SECURE. Malformed ports and booleans are refused by variable name. An unset port lets the driver choose 8123, or 8443 when secure. The CLI, project scaffolds and examples now use it, so scaffolded apps honourCLICKHOUSE_SECURE.ClickHouse executors are context managers (
with/async with) that close their connections on exit.The async executor's cancellation-control connection now uses the same 2-second receive timeout as the sync executor's.
Fix deployment contracts that widened a filter declared with an empty
operatorsallow-list to every operator. The empty list now reaches protocol validation and is rejected, matching TypeScript, so a deployment can no longer accept filters the local planner refuses.Fix
hypequery initandhypequery generate datasetsemitting a SQL-backedtotalCountmeasure without dependencies, which made every generated project fail to build a deployment contract and refused relationship joins.totalCountnow counts the first supported non-nullable column. All-nullable tables instead receive the explicitly namednonNullCount, with a warning that it excludes nulls.Fix deployment of measure filters with integer values, such as
filter.gte("amount", 10), which failed withHQ_VALUE_INTEGER_TAG_REQUIRED. An exactly representable integer now publishes as the same binary64 number TypeScript writes, with an identical deployment identity. Integers beyond 2**53 are refused with a clear error rather than rounded.TenantDatasetClient.executeandAsyncTenantDatasetClient.executeacceptpaginate=True, as the unbound clients do, so request handlers holding a tenant-bound client can fetch pages withhas_moremetadata.Bind timestamp parameters as Unix seconds. RFC 3339 filter values such as
2026-10-25T01:30:00Zwere rejected by ClickHouse, and aware datetimes could resolve to the wrong instant on a non-UTC server in a repeated daylight-saving hour. Values without an offset are passed through unchanged.Add
run_devandpython -m hypequery.serve.dev app:appfor local development. The runner binds to loopback by default, warns withExternalBindWarningwhen bound beyond it, supports--reload, and refuses apps created with aProductionProfile.Add a README Getting started path from install to a served dataset, run as written from a built wheel against ClickHouse in CI.
Add a validated ASGI production profile and Uvicorn runner with explicit bind and proxy trust, startup refusals for debug/docs/reload/cookie auth, bounded admission, cancellation-aware deadlines and database/HTTP result ceilings.
Qualify shared HTTP fixtures under the production profile, test the real runner and add the production HTTP path to the live ClickHouse matrix.
Add authenticated dataset and measure-backed metric HTTP endpoints with role, scope, tenant, and page-size policy; support synchronous and asynchronous clients.
Add accurate over-fetch pagination without changing ordinary client executions.
Return public operational metadata separately from authorized, audited, redacted SQL diagnostics. Semantic measures use strings on the HTTP wire, matching TypeScript; local client result values retain their native types.
Propagate disconnect and handler cancellation to execution; optionally emit validated, metadata-only RFC 0011 success and failure events.
Add bounded logical discovery and an application factory with docs and OpenAPI disabled by default. Development documentation requires explicit opt-in.
Run shared semantic HTTP fixtures in Python and TypeScript CI, including the FastAPI and Uvicorn floors.
Breaking (pre-release): every authoring helper now lives in exactly one namespace:
dimension.*,measure.*,filter.*,order.*and the newformula.*(formula.divide,formula.null_if_zero,formula.round, …).hypequery.datasetsno longer re-exports the loose helpers. That covers the aggregations (sum,count,min,max, …), the filters (eq,in_list, …), the orderings (asc,desc) and the formula functions (divide,round, …). The package no longer shadows Python builtins such assum,min,maxandround. Writemeasure.sum("amount"),filter.eq("status", "paid"),order.desc("revenue")andmeasure.derived(formula.divide("a", "b")).Breaking (pre-release):
hypequery.serve.validate_correlation_idis nowsanitize_correlation_id. The new name says it returns the caller's id orNoneinstead of raising. It no longer shares a name with the planner'svalidate_correlation_id, which raises.create_apiandstart_serverare now the definitions, matching TypeScript, and their error messages use those names.create_routerandrun_productionremain as aliases.Add
measure.derived(formula)over local numeric measures, including validated references/dependency cycles, aggregate planning, HAVING, HTTP serving, and safe discovery.Emit derived formulas and inputs in catalogs, semantic snapshots and deployment contract 2, with shared TypeScript fixtures. Chained derived measures execute locally but cannot be exported under contract 2; relationship traversal remains limited to base measures.
Formula limits and literal types match TypeScript: at most 256 nodes and 16 levels per formula, and integer literals keep integer arithmetic (
add("orders", 1)returns an integer).Serialize relationship-qualified measures as strings (preserving NULL), like local measures, without changing dimension values or native client results.
Allow
X-Include-Metain the default CORS request-header policy so React infinite dataset and metric queries can request pagination across origins.Stop suggesting
totalCountfor tables with no supported non-nullable column. GeneratenonNullCountwith an explicit warning instead, and use it in the scaffold example. It counts non-null values, not every row. Review existingtotalCountreferences when regenerating definitions for these tables.Add explicit
time_grainspolicies to Python datasets, enforced by the planner and dataset/metric HTTP endpoints and reflected in catalogs, semantic contracts and OpenAPI. Unsupported grains are rejected with TypeScript's message,Unsupported time grain "hour". Supported: day, month.Deployment export refuses a policy that excludes any day-through-year grain, because contract 2 cannot preserve it. A policy that includes every one of those grains publishes unchanged, matching TypeScript.
Add validated query and client timezones (UTC by default), apply the zone to time buckets and local time-key filter bounds, and accept timezone overrides on dataset and metric HTTP endpoints.
Behavior change: time buckets and offset-free time-key bounds previously used the column's or server's timezone; they now use the query zone, UTC unless set. Pass
timezone=tocreate_dataset_clientto keep a non-UTC zone. Persistent result caches populated before upgrading can return old buckets until their entries expire.Partition non-UTC cached results using the same deployed-identity convention as TypeScript.
Behavior change:
minute,houranddaybuckets now returnperiodas wall-clock text in the query timezone,YYYY-MM-DD HH:MM:SS(RFC 0015), matching TypeScript. They were previously UTC ISO instants such as2026-01-02T05:00:00+00:00. Week-and-longer buckets are unchanged (YYYY-MM-DD). A shared fixture,specs/datasets/period-format-v1.json, now pins the form in both languages.Add Python release automation with opt-in canaries after successful main CI, tagged beta/RC/stable releases, build-only rehearsals, artifact and CLI checks, and token-free PyPI Trusted Publishing.
Prepare Python beta/stable release PRs with Towncrier, explicit version selection, synchronized package metadata and lockfile, preserved changelog history, and Python CI against the generated release commit.
Audit locked and minimum runtime dependency resolutions across supported Python versions, and retain release SBOMs, audit reports and distribution checksums.
Require urllib3 2.8.0 or newer for ClickHouse execution, fixing three published advisories. Raise core and serving dependency floors to the versions qualified by the beta suite.
Preserve nulls for unmatched related dimensions and filters independently of ClickHouse
join_use_nulls, including queries without related measures. Missing or other-tenant targets no longer appear as empty strings or zero values.ResultCache(on_error=...)observes cache failures that would otherwise be silent. Failures still never fail a query. The hook receives the stage ("key","get"or"put") and the exception; without it, failures are logged at debug level by exception type only.ExecuteOptionstypes the options tenant-bound clients forward, andDEFAULT_PAGE_SIZEnames the default page size.Select one-hop relationship base measures as
<relationship>.<measure>, matching@hypequery/datasets. Duplicate-insensitive aggregates are allowed throughbelongs_toand every aggregate throughhas_one. Unmatched target rows never feed an aggregate, and target tenant and fixed-filter scoping are preserved. Catalogs, ordering and discovery advertise the safe names.Reject a query that selects one name as both a dimension and a measure.
Add
havingconditions on selected measure values, matching@hypequery/datasets(HQ-329). Operators areeq,neq,gt,gte,lt,lte,between,inandnotIn. Values are finite numbers bound as parameters, and conditions count againstmax_filters. Dataset endpoints forwardhaving; metric endpoints refuse it. Queries with conditions bypass the result cache until the cache preimage carries them.Support composite relationship keys (RFC 0016), matching
@hypequery/datasets. Passkeys=(("customer_id", "id"), ("region_code", "region"))to a relationship helper; every pair is AND-ed in the join, and related-measure projections keep every target key. Catalogs, semantic contracts and deployment contracts carrykeys, and protocol validation accepts and checks them. Single-key relationships are unchanged.Align Python CLI names with TypeScript:
init --path,dev --hostname,-p,--no-watch,-Vandhelp [command]; preserve positional destinations,--hostand--no-reloadas aliases.Add framework-free
hypequeryandpython -m hypequeryCLI entry points, help/version and theinit/devcommand contract (PYE-01A).Add
hypequery init [directory]with packaged Python project templates, authenticated orders endpoint, setup instructions and collision protection (PYE-01B).Inspect the configured ClickHouse schema during
hypequery init, generate typed dataset definitions and a schema snapshot, and serve selected tables through authenticated endpoints. Match TypeScript's table selection and semantic naming; report unsupported columns, collisions and tenant candidates. Count nullable rows accurately. The offline demo requires--skip-connection.Add
hypequery dev [module:app]with reload enabled by default, safe local runner policies,--no-reload, and actionable startup errors (PYE-01C).Gate Python CLI onboarding on wheel/sdist installs outside the checkout across Python 3.11–3.14, including a real authenticated ClickHouse query (PYE-01D).
Verify schema-backed generation from installed wheel/sdist, serve discovered tables, and compare query results with raw ClickHouse SQL. Keep explicit offline example checks for base installations without driver extras.
Align Python public authoring names with TypeScript: add
dataset,dimension.string/number/boolean/timestamp, basemeasurehelpers,filterandordernamespaces,create_memory_cache_store,create_api, endpoint factories,serve_dev, andstart_server. Existing constructors, callable dimension helpers andrun_devremain available; use the namespaces above for aggregation, filter, ordering and formula helpers.Add
hypequery generate datasets(aliasgenerate:datasets) using live ClickHouse metadata, with TypeScript-equivalent--output,--path, table selection,--diff,--check,--forceand--tenant-columnoptions. It defaults todatasets.py, matching the project layout created byhypequery init. Read-only drift checks exit 1 without writing; replacement is atomic and preserves the existing file's permissions.--forcereplaces the whole file and warns when it removes a configuredtenant_key;--tenant-columnsetstenant_keyon tables that have that column and reports tables that do not.Reject destinations changed during discovery or writing, coordinate CLI writers through replacement, and accept concurrently created parent directories. Installed wheel/sdist journeys now restart serving and compare the regenerated column's query response with raw SQL.
Add explicit synchronous and asynchronous relationship uniqueness checks. Checks respect trusted tenant scopes, deadlines and cancellation, validate complete composite target keys, and report duplicate to-one keys without automatically running database queries.
Require aiohttp 3.14.4 or newer with the
clickhouse-asyncextra, fixing three published advisories (CVE-2026-111063, CVE-2026-111064, GHSA-2g87-pp6c-29x5).Retry server-side
KILL QUERYfrom the async executors until ClickHouse confirms it or the driver call returns. A cancellation or deadline that landed before the query registered on the server previously let the query run tomax_execution_timewhile holding its worker slot; the synchronous executor already retried.AsyncFromSyncClickHouseExecutorkeeps retrying for as long as its driver call runs, so a submission slower thanmax_execution_timecannot escape cancellation; the native async executor stops when its driver request ends if the server cancels read-only queries on a closed request (clickhouse-connect enables this whenever the user may change it), and otherwise at the server's own time limit.aclose()on either async executor stops its outstanding retries.The result cache treats a stored value that cannot answer the query as a reported
getfailure and a miss. That covers anything other thanCachedRows, rows that don't match the columns, non-scalar values, and columns that differ from the query's. Previously a malformed value failed the request, and a well-formed entry with other columns was served as a hit.ResultCache.getaccepts the expectedcolumns.Canonicalize text containing unpaired UTF-16 surrogates as
JSON.stringifydoes, writing a\\udXXXescape, instead of failing with aUnicodeEncodeError. Deployment and bundle contracts with such a label or description now produce the same canonical bytes as TypeScript.hypequery initandhypequery generate datasetsnow work for least-privilege ClickHouse users. Schema discovery sentreadonly = 1on every catalog query, so areadonly = 2user failed with "Cannot modify 'readonly' setting" and areadonly = 1user was refused on the query limits. Discovery now follows the executor: it leaves outreadonlyfor a user whose profile is already read-only, keeps the time and row limits forreadonly = 2, and relies on a SQLLIMITforreadonly = 1, which may not change any setting.When discovery fails,
initandgeneratenow name what to check, asdevdoes: credentials, host and port, database, or grants. Previously every failure gave one generic message.hypequery devkeeps its documented exit codes: a busy port now exits 1 with a hint naming the port, instead of exiting 3. A missing environment variable, such asHYPEQUERY_DEV_TOKEN, is named instead of reported as a generic failure. ClickHouse driver warnings, such as "Unexpected Http Driver Exception" and urllib3 retry lines that can include bound parameter values, no longer print ahead of the CLI's own hint.hypequery initprintscd <directory> && hypequery devwhen the project was created somewhere else. The generated.env.examplelistsCLICKHOUSE_SECURE. Theexport client-contract --checkhint now says to review with--stdoutand replace with--force.Canonicalize client-contract numbers beyond 2**53 with ECMAScript's shortest round-trip digits, matching TypeScript's
JSON.stringify. Contract identities and schema hashes containing such a value no longer differ between the two implementations.serialize_semantic_contractandhash_contractno longer raiseUnicodeEncodeErrorwhen a label or description contains an unpaired UTF-16 surrogate. Such a character is escaped as\udXXX, asJSON.stringifydoes, so the hash matches TypeScript's for the same model.hypequery.protocolnow exportsserialize_protocol_string.Lower the dependency floors to measured minimums: pydantic 2.9.2, FastAPI 0.137.2, Starlette 1.3.1, Uvicorn 0.30.6 and clickhouse-connect 1.4.2 (urllib3 stays at 2.8.0). Each is the oldest release that passes the full suite, so
hypequeryinstalls next to projects that pin older releases of those libraries.Add
validate_deployable(dataset), which refuses derived measures the deployment contract cannot carry (a reference to another derived measure, or a literal anywhere other thanrounddecimals or acoalescefallback) with an error naming the dataset and measure, for exampleorders.doubled: a numeric literal cannot be an arithmetic operand in a deployable formula.build_protocol_deployment_contract()andwrite_dataset_bundle()run the check first instead of failing with only a contract path.Order time buckets by their instant rather than their text label. On a daylight-saving fall-back day, two hourly buckets share a label such as
01:00:00, and they could come back in either order. TypeScript already orders them chronologically.The
embedded_analyticsexample is now tenant-scoped. Its dataset declarestenant_key="org_id", the endpoint requires a tenant, and the example token belongs to organization "a". Previously every caller saw every organization's orders.Served dataset and metric endpoints no longer expose related datasets' fields by default. Previously a caller refused by the shipments endpoint could still read
shipment.carrierthrough the orders endpoint, and the client contract advertised those fields. Related dimensions, related measures, and named filters defined on a related field now needinclude_related={"shipment"}oncreate_dataset_endpoint,add_dataset_endpointor the metric equivalents. Without it, they are refused with400 VALIDATION_ERRORand left out of the endpoint's client contract. So are measures whose fixed filters read a related field, directly or through a derived measure; an omittedmeasureslist selects only the base measures the endpoint can serve, and a metric endpoint for such a measure is refused when created.ClientEndpointDefinitiongainsrelatedfor the same purpose, with the same default and checks, so an offlinehypequery export client-contractmatches the live contract of an endpoint installed with the same arguments. Passrelated={"shipment"}where the endpoint passesinclude_related={"shipment"}; an offline metric whose measure filters through an excluded relationship is refused, as Serve refuses it.Report a filter, ordering or having field name outside the portable identifier grammar as an
input-invalidquery error fromDatasetClient.validateandexecute. It previously escaped as a rawProtocolIdentifierError(aTypeError), which non-Serve hosts answered as a server fault.Add
control_usernameandcontrol_passwordtoClickHouseConnection, read fromCLICKHOUSE_CONTROL_USERNAMEandCLICKHOUSE_CONTROL_PASSWORDbyfrom_env(), so the connection that sendsKILL QUERYcan sign in as its own user. Without them, the query user cancels its own queries and needsGRANT SELECT ON system.processes; a separate control user also needsKILL QUERY. The README now documents both grants.When ClickHouse refuses a
KILL QUERY, executors now log one error naming the missing grant and stop retrying, instead of sending a warning and dozens of refusedKILLs per cancelled query.validate_protocol_client_query(..., transport="mcp")refuses alimitabove the endpoint'smaxLimitinstead of clamping it, matching TypeScript's MCP executor. HTTP input still clamps.Return
nullfor non-finite measure values instead of failing the query. ClickHouse reportsnanforavg,stddevandvarianceover no matching rows andinffor a derived division by zero; such results previously surfaced as an internal error, so a metric endpoint whose filters matched nothing answered 500.Refuse integer filter values outside ±2**53 on number fields with
input-invalid. Number filters bind asFloat64, which can't hold larger integers exactly, so ClickHouse compared a rounded value and an exactUInt64orInt64id silently matched nothing. Declare a large id asdimension.string()to filter it exactly.Ship the Apache-2.0
LICENSEfile in the wheel and sdist, and leave a stale internal review document out of the sdist.Raise transitive minimum versions that the direct floors still admitted with published advisories:
h11>=0.16.0(via uvicorn) andanyio>=4.14.2(via Starlette) in thefastapiextra,certifi>=2026.10.19(via clickhouse-connect) in both ClickHouse extras, andidna>=3.15(via anyio and aiohttp) in thefastapiandclickhouse-asyncextras.create_async_clickhouse_executornow raisesModuleNotFoundErrornaminghypequery[clickhouse-async]when aiohttp is missing. With onlyhypequery[clickhouse]installed, it previously failed with an opaqueinternalerror.Rewrite the README's beta notice for PyPI, where the old relative link was broken. Document what each install extra is for, including
clickhouse-async.hypequery devnow says which ClickHouse setting to check when the app cannot connect: refused credentials, an unreachable host, a missing database or denied access each get their own hint instead of "App could not start". Wrong-password failures, which ClickHouse reports asREQUIRED_PASSWORD,WRONG_PASSWORDorUNKNOWN_USER, are now classifiedunauthenticatedrather thaninternal, and an unknown database isnot-found.A ClickHouse user whose profile sets
readonly = 1refuses HypeQuery's per-query limits; that failure is now reported asforbiddenwith guidance to use areadonly = 2user instead of an opaque internal error.Deadlineis exported fromhypequery.datasetsso anExecutionContextdeadline can be built from the public package.Passing a dataset name instead of a
Datasettobelongs_to,has_oneorhas_manyraises aTypeErrorthat says so, instead of anAttributeError.AsyncFromSyncClickHouseExecutorno longer leaves "Future exception was never retrieved" in the logs when a caller cancels a query.README: the relationship example names the physical
customer_idcolumn (join keys are columns, not dimensions), the result cache documents a store failure as amiss, and the bridge executor documents the session-free driver clients it needs.Report a query that exceeds its result limit as
too-large(HTTP 413) on ClickHouse 26, which renamed the error toTOO_MANY_ROWS_OR_BYTES(code 396). It was reported asinternal(HTTP 500).Exclude the Hypothesis example database from source distributions and refuse archives that contain one in the release artifact audit.
Serve routes now map Starlette's
HTTPExceptionlike FastAPI's, keeping its status and stringdetail. They previously returned500for it, and FastAPI raises it itself for a body it cannot parse. So a body with invalid UTF-8, or one nested past the recursion limit, now gets400 VALIDATION_ERRORinstead of500, without logging a traceback.Serve routes refuse a request body with any
Content-Encodingother thanidentitywith415. A gzip body previously got500. The body cap counts bytes as they arrive, so a compressed body is never expanded.Fix Serve requests that could hang until the client gave up, or until the production timeout (30 s by default), after the query had already finished. The request's disconnect watcher is now stopped with a flag rather than
Task.cancel(), which Starlette'sRequest.is_disconnected()could absorb. The stall hit sequential requests too, and became frequent under concurrency or host load: about 60% of requests at 50 concurrent.Refuse timestamp filter values that are not a real ISO 8601 date or date-time, such as
2026-13-45,2026-02-30,yesterdayor2026-01-31 12:00, withinput-invalidnaming the filter. ClickHouse coerced some of these to a different instant instead of failing:2026-13-45became 1900-01-01 and2026-02-30rolled over to March 2, so the query silently returned wrong numbers.validate()now reports them too. Accepted forms areYYYY-MM-DD, orYYYY-MM-DD[T ]hh:mm:ss[.fraction], optionally followed byZor±hh:mm.likepatterns on timestamp fields are unchanged.Require pydantic 2.12.2 or newer on Python 3.14. pydantic-core publishes no Python 3.14 wheel for the previous 2.9.2 floor, and pydantic 2.12.0 rejects one of the serve request models on 3.14. Python 3.11–3.13 keep the 2.9.2 floor.
The README that
hypequery initgenerates installs the pinned SDK from PyPI withpip install -e ., keeping the local-wheel route for unreleased builds.