Conversation
Adds `clickhouseServers` to the environment-level DuckDB connection. Each entry
registers a DuckDB table macro named after the server, so a model writes
`demo('SELECT ...')` and builds a source on the result.
ClickHouse's HTTP interface can return Parquet and DuckDB can read Parquet over
HTTP, so the macro is the entire integration -- no driver, no Malloy dialect, no
ClickHouse extension. The `chsql` community extension packages the same idea as
`ch_scan`, but it has no build past DuckDB 1.3.2 and Malloy embeds 1.5.3, and
its macro neither URL-encodes the query nor supports password auth.
Config declares only data-source intent, matching `attachedDatabases`: Publisher
owns the URL, the encoding, and the credential handling. Credentials become an
HTTP Basic header in a DuckDB secret scoped to the server's base URL, so they
stay out of the query URL (ClickHouse logs URLs in `system.query_log`) and out
of the macro body (readable by any model author via `duckdb_functions()`).
Nothing is pushed down: the macro argument is what ClickHouse runs, and Malloy
aggregates whatever came back. Measured on a 5M-row table, aggregating inside
the argument rather than extracting rows was 156 ms against 1961 ms, and 3 KiB
across the wire against 1.63 MiB, for identical results. The docs and the
example lead with that rule.
An env-level DuckDB connection previously had to declare at least one attached
database; it now needs at least one of the two, and the error message says so.
Adds docs/clickhouse.md, an examples/clickhouse package with a seeded
docker-compose, unit tests against a real DuckDB, and an integration test gated
on CLICKHOUSE_TEST_HOST.
Signed-off-by: James Swirhun <james@credibledata.com>
The env-level DuckDB connection now accepts clickhouseServers as a data source, so its "nothing configured" error changed. tests/unit/duckdb (run by test:integration, not test:unit) pinned the old wording and was missed. Also covers the new accept case there. Signed-off-by: James Swirhun <james@credibledata.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
clickhouseServersto the environment-level DuckDB connection, so a Malloy model can query a remote ClickHouse server:{ "name": "clickhouse", "type": "duckdb", "duckdbConnection": { "clickhouseServers": [ { "name": "demo", "host": "localhost", "port": 8123, "database": "demo", "user": "malloy", "password": "${CLICKHOUSE_PASSWORD}" } ] } }Each entry registers a DuckDB table macro named after the server:
Why this shape
ClickHouse's HTTP interface returns Parquet and DuckDB reads Parquet over HTTP, so a table macro is the entire integration — no driver, no Malloy dialect, no ClickHouse extension.
The
chsqlcommunity extension packages the same idea asch_scan, and it is worth saying why it is not used: its builds stop at DuckDB 1.3.2 while Malloy embeds 1.5.3 (INSTALL chsql FROM community404s), and dumping its macro shows it concatenates the query withouturl_encodeand takes auserbut no password. SoSELECT 1 + 1returns HTTP 400 and any password-protected server returns 403. Both are fixed here.Config declares only data-source intent, matching the existing
attachedDatabasescontract — Publisher owns the URL, the encoding, and the credentials.Credential handling
Credentials become an HTTP Basic header stored in a DuckDB secret scoped to the server's base URL. They are deliberately kept out of two places:
system.query_logduckdb_functions()passwordgoes through the existing${ENV_VAR}substitution, so it need not sit in the config file. Tests assert the password appears in neither place, and that a wrong password fails rather than silently returning nothing.The caveat, stated up front
Nothing is pushed down. The macro argument is exactly what ClickHouse runs; Malloy's filters and
group_byare applied by DuckDB to the rows that came back. Aggregate inside the argument.Measured on a 5M-row table, same results both ways:
SELECT *, aggregate in DuckDBThe docs, the example model, and the schema description all lead with this rather than leaving it to be discovered.
Behaviour change
An env-level DuckDB connection previously had to declare at least one attached database. It now requires at least one of
attachedDatabasesorclickhouseServers, and the error message names both. Two existing tests asserted the old wording and were updated (one intest:unit, one intests/unit/duckdb, which is run bytest:integration).Validation
Bad config fails at startup with an actionable message rather than registering a macro nobody can call:
primary) → rejected, since a model callingprimary('…')would not parse. Checked against DuckDB's ownduckdb_keywords()so it cannot drift from the parser across upgrades.Testing
clickhouse_servers.spec.ts(20 tests, always run) — exercises a real in-memory DuckDB rather than asserting on generated SQL strings, so DuckDB's parser and secret manager are the oracle. This caught a real bug during development:primarymatched the identifier regex but failed to parse as DDL, which is what prompted the reserved-word check.clickhouse_servers.integration.spec.ts(5 tests, gated onCLICKHOUSE_TEST_HOST) — runs against a live ClickHouse: scalar round-trip,+/&survival, type mapping, server-side aggregation, and bad-password rejection.Each unit test gets its own DuckDB database file. Malloy shares one DuckDB instance across
:memory:connections whose config hashes match, so with:memory:the absence assertions passed alone and failed in the full suite as other spec files leaked macros and secrets in.Docs & example
docs/clickhouse.md— config, the aggregate-server-side rule with the measurements, the ClickHouse→DuckDB type mapping, and theDateTimetimezone gotcha.examples/clickhouse/— runnable package with adocker-compose.ymlthat seeds 50k rows, and two sources contrasting the pre-aggregated shape against the row-grain shape. Not in the default environment, since it needs a running ClickHouse.docs/connections.mdupdated — it stated that attached databases were the only configuration available and that one was required. Both are now false.Notes for review
httpfsis already baked into the Publisher image (8/8 extensions), so this works underEXTENSION_FETCH_POLICY=local-only.api-doc.yamlis the only tracked source for the new schema.