Skip to content

The ClickHouse edge answers system.*, DESCRIBE, SHOW and EXISTS from an emulated catalog (T-482, T-483) - #352

Merged
chasers merged 2 commits into
t-493-clickhouse-hyperdx-formatsfrom
t-482-clickhouse-system-catalog
Sep 20, 2026
Merged

chasers merged 2 commits into
t-493-clickhouse-hyperdx-formatsfrom
t-482-clickhouse-system-catalog

Conversation

@chasers

@chasers chasers commented Sep 20, 2026

Copy link
Copy Markdown
Owner

TL;DR: The ClickHouse edge answers system.*, DESCRIBE, SHOW and EXISTS from an emulated catalog, built the way the Postgres edge builds pg_catalog.

Tracker: T-482, T-483. Plan PL-66. Stacked on #351 (T-493).

Why

  • HyperDX asks system.settings before its first query. If that fails, every query after it fails.
  • It reads columns with DESCRIBE and the sorting key from system.tables.
  • All of these answered "unknown table".
  • SHOW TABLES was worse: it listed DuckDB's own empty catalog. A wrong answer, not an error.

What changed

  • New SmolqueryClickHouse.SystemCatalog: a GenServer per edge with a private engine.
  • The edge runtime gains :catalog, resolved as the Postgres edge's is.
  • serialize, base_tables, listed_tables move from PgCatalog to Smolquery.CatalogEmulation. Both edges use them.

What answers

Statement Answer
system.databases, tables, columns From smolquery's catalog, rebuilt at most once a second
system.settings, data_skipping_indices No rows
system.table_engines, system.one Static
DESCRIBE [TABLE] db.t Seven columns, as ClickHouse answers
SHOW DATABASES, SHOW TABLES [FROM db], EXISTS [TABLE] db.t From the same tables
  • A table's engine is MergeTree. Its sorting_key and primary_key are the clustering key.
  • A column's type is the ClickHouse type an insert reads it as.

How it decides

  • A SELECT is the catalog's only when DuckDB's parse says every table it reads is a system one.
  • system.x is written system_x first. A bare table column is quoted: it is a reserved word to DuckDB.

Watch out

  • ⚠️ A system table that is not emulated is code 60, by its ClickHouse name.
  • ⚠️ A catalog that cannot be read is a retryable 503. Never an empty answer.
  • ⚠️ Not done: system.numbers (needs a table function), SHOW CREATE TABLE.
  • ⚠️ The edge now reads the lake's catalog through its own engine, as the Postgres edge does.

How to review

  1. system_catalog.ex — the moduledoc table, then read/2 and classified/2.
  2. catalog_emulation.ex — moved code, no change.
  3. system_catalog_test.exs — the statements are HyperDX's, verbatim.

Checks

  • mix precommit
  • mix ci
  • mix dialyzer

Review fixes

  • 🔒 Host file read fixed. SELECT * FROM system.one, read_text('/etc/hostname') answered the file. The catalog's engine now has external access off and its configuration locked.
  • 🔒 A statement naming a table function anywhere, or a RECURSIVE table expression, is not the catalog's.
  • ✅ A statement gets 10 s and 10,000 rows, in a task. Outliving it answers a retryable 503 and no longer restarts the listener.
  • ✅ A literal such as 'system.cpu' no longer asks the catalog server anything.
  • ✅ Mirrored test file for Smolquery.CatalogEmulation.

🤖 Generated with Claude Code

Chase Granberry and others added 2 commits September 20, 2026 02:42
…an emulated catalog (T-482, T-483)

A ClickHouse client reads the catalog before anything a user typed.
HyperDX asks system.settings before its first query and fails every query
after if that one fails; it reads columns with DESCRIBE and the sorting key
from system.tables. clickhouse-go sends DESCRIBE TABLE before each batch.
All of it answered "unknown table", and SHOW TABLES answered DuckDB's own
empty catalog, which is a wrong answer rather than a failure.

SmolqueryClickHouse.SystemCatalog is built the way SmolqueryPg.PgCatalog
is: a GenServer per edge owning a private engine. system.databases, tables
and columns are rebuilt from Smolquery.Catalog at most once a second;
settings and data_skipping_indices are empty; table_engines and one are
static. A table's engine is MergeTree, its sorting_key and primary_key the
clustering key, a column's type the ClickHouse type an insert reads it as.

- DESCRIBE [TABLE], SHOW DATABASES, SHOW TABLES [FROM] and EXISTS [TABLE]
  are read at the edge and answered from those tables.
- A SELECT is the catalog's only when DuckDB's parse says every table it
  reads is a system one. system.x is written system_x first, and a bare
  "table", a reserved word to the engine, is quoted.
- A system table that is not emulated is code 60 by its ClickHouse name; a
  catalog that cannot be read is a retryable 503, never an empty answer.
- The edge runtime gains :catalog, resolved as the Postgres edge's is.

serialize, base_tables and listed_tables move out of PgCatalog into
Smolquery.CatalogEmulation, since both edges now need them and ex_dna
would rightly refuse a second copy.

Tested with the statements HyperDX sends, verbatim, parameters included.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r, caps its answer, outlives a slow statement, and is not asked about a literal

The edge's emulated catalog runs a client's SELECT in its own DuckDB
engine, and that engine was started bare. The review read a host file
through it:

  GET /?query=SELECT * FROM system.one, read_text('/etc/hostname')

The only gate looked at BASE_TABLE nodes, so a table function beside a
system table passed, and it passed the GET read-only check too. The job
engines that run a user's SQL are locked down by the query service; this
one was not.

- The engine has external access off and its configuration locked as soon
  as its tables exist. No statement reads a file or a URL, whatever reaches
  it. A test asks the engine directly.
- A statement that names a table function anywhere in its AST, or a
  RECURSIVE table expression, is not the catalog's. It has nothing to read
  here but a generator, and range(100000000000) has no end.
- An answer is cut at 10,000 rows and a statement is given 10 seconds, in a
  task. A statement that outlives it answers a retryable 503 and leaves the
  server standing. Before, Engine.frame's call exit crashed the GenServer,
  and rest_for_one restarted admission and the listener under it, dropping
  every request in flight.
- mentions_catalog? read the raw statement, so WHERE MetricName =
  'system.cpu.utilization' paid a call to the server and three parses to
  learn it was not a catalog statement, and answered 503 when the server
  was busy. It reads code tokens now.

Smolquery.CatalogEmulation gets the mirrored test file AGENTS.md asks for,
and the runtime's three new names are tested.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chasers
chasers force-pushed the t-482-clickhouse-system-catalog branch from 363b61f to f2a6575 Compare September 20, 2026 03:11
@chasers
chasers merged commit b01a026 into main Sep 20, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant