ax-check.com
by Gauge
C
ClickHouse
clickhouse.com
ax-check.com
Checked 8 November 2026
A
100/100
Agents need your login
Agents can install, query, and price ClickHouse with zero guesswork.
All 23 checklist items passed: docs, CLI, API, MCP, and skills are all machine-readable, and pricing is published as exact USD rates with clear units.
1 fix to start with
Concrete changes, backed by what we found.
Recorded coding sessions
Same task, 3 models, no clickhouse.com account. Recorded 8 November 2026 on Pi.
3 recorded · 5m 34s
5m 34s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
I need to investigate this further. Please let me confirm the details.
ls -la /sandbox/repo && find /sandbox/repo -maxdepth 2 -type f | head -50
total 16
total 16 drwxr-xr-x 3 root root 4096 Sep 18 08:00 . drwxr-xr-x 7 root root 4096 Nov 8 02:07 .. drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git -rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore /sandbox/repo/.gitignore /sandbox/repo/.git/COMMIT_EDITMSG /sandbox/repo/.git/config /sandbox/repo/.git/index /sandbox/repo/.git/HEAD /sandbox/repo/.git/description
Recorded tool call
{
"type": "tool_call",
"seq": 3,
"ts": "2026-11-08T02:07:21.032Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls -la /sandbox/repo && find /sandbox/repo -maxdepth 2 -type f | head -50"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba68fa5e4beda39b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 4,
"ts": "2026-11-08T02:07:21.032Z",
"turn": 1,
"output": {
"content": [
{
"text": "total 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Nov 8 02:07 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n/sandbox/repo/.gitignore\n/sandbox/repo/.git/COMMIT_EDITMSG\n/sandbox/repo/.git/config\n/sandbox/repo/.git/index\n/sandbox/repo/.git/HEAD\n/sandbox/repo/.git/description\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba68fa5e4beda39b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The repository is empty except for .gitignore. In other words, this is a from-scratch task. Let's check the available tools (is the clickhouse client installed? What about Python? What about pip?) and whether credentials are available.
cat /sandbox/repo/.gitignore; echo "---GIT---"; cd /sandbox/repo && git log --oneline -5 2>&1 | head
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs. # Logs / OS cruft *.log .DS_Store # JavaScript / TypeScript node_modules/ .next/ dist/ build/ coverage/ # Python .venv/ venv/ __pycache__/ *.py[cod] .pytest_cache/ *.egg-info/ # Rust target/ # Go / PHP / Ruby vendored deps vendor/ .bundle/ # Java / Kotlin / Scala .gradle/ # .NET bin/ obj/ # Elixir _build/ deps/ # Dart / Flutter .dart_tool/ ---GIT--- 9ebf5fb Initial workspace
Recorded tool call
{
"type": "tool_call",
"seq": 7,
"ts": "2026-11-08T02:07:23.244Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cat /sandbox/repo/.gitignore; echo \"---GIT---\"; cd /sandbox/repo && git log --oneline -5 2>&1 | head"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-af441c5ae0867b4a",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 9,
"ts": "2026-11-08T02:07:23.244Z",
"turn": 1,
"output": {
"content": [
{
"text": "# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n---GIT---\n9ebf5fb Initial workspace\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-af441c5ae0867b4a",
"is_error": false,
"tool_kind": "shell"
}which clickhouse clickhouse-client python3 pip3 node npm curl 2>&1; echo "---ENV---"; env | grep -iE 'click|ch_|CLICKHOUSE' | sed 's/=.*/=<set>/'; echo "(end env)"; python3 -V
/usr/local/bin/python3
/usr/local/bin/python3 /usr/local/bin/pip3 /usr/local/bin/node /usr/local/bin/npm /usr/bin/curl ---ENV--- (end env) Python 3.12.3
Recorded tool call
{
"type": "tool_call",
"seq": 8,
"ts": "2026-11-08T02:07:23.244Z",
"name": "bash",
"turn": 1,
"input": {
"command": "which clickhouse clickhouse-client python3 pip3 node npm curl 2>&1; echo \"---ENV---\"; env | grep -iE 'click|ch_|CLICKHOUSE' | sed 's/=.*/=<set>/'; echo \"(end env)\"; python3 -V"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ac5a15a31ae4ed63",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 10,
"ts": "2026-11-08T02:07:23.244Z",
"turn": 1,
"output": {
"content": [
{
"text": "/usr/local/bin/python3\n/usr/local/bin/pip3\n/usr/local/bin/node\n/usr/local/bin/npm\n/usr/bin/curl\n---ENV---\n(end env)\nPython 3.12.3\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ac5a15a31ae4ed63",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
ClickHouse credentials and client aren't installed. So I need to use the hosted product via SDK/API. ClickHouse Cloud requires credentials (URL, user, password). No credentials in the environment variables. Let me check if there's some kind of credentials file.
The task says "use the hosted product via SDK or API." ClickHouse Cloud is a hosted product, but it requires credentials. There's also the public ClickHouse playground: https://play.clickhouse.com/?user=play, which is a free hosted ClickHouse instance accessible via HTTPS API without authentication. This is the legitimate approach — no local stack needed, no long-running commands needed.
Let me test connectivity to play.clickhouse.com.
Also check the network in general (there might be no internet).
Let me try: curl 'https://play.clickhouse.com/?user=play' --data 'SELECT 1'
Also check whether the clickhouse_connect Python package is pip-installable (is there internet to PyPI?).
Let me run both in parallel.
cd /tmp && timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data 'SELECT version()' ; echo "EXIT=$?"
26.10.3.7
26.10.3.7 EXIT=0
Recorded tool call
{
"type": "tool_call",
"seq": 13,
"ts": "2026-11-08T02:07:27.292Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data 'SELECT version()' ; echo \"EXIT=$?\""
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba59a2f4e59da267",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 15,
"ts": "2026-11-08T02:07:28.078Z",
"turn": 1,
"output": {
"content": [
{
"text": "26.10.3.7\nEXIT=0\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba59a2f4e59da267",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && timeout 60 pip3 install --quiet clickhouse-connect 2>&1 | tail -5; echo "EXIT=$?"; python3 -c "import clickhouse_connect; print('clickhouse_connect', clickhouse_connect.__version__)" 2>&1 | tail -3[notice] A new release of pip is available: 26.2.1 -> 26.3
[notice] A new release of pip is available: 26.2.1 -> 26.3 [notice] To update, run: /opt/freestyle/python/bin/python3 -m pip install --upgrade pip EXIT=0 clickhouse_connect 1.12.0
Recorded tool call
{
"type": "tool_call",
"seq": 14,
"ts": "2026-11-08T02:07:27.292Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 60 pip3 install --quiet clickhouse-connect 2>&1 | tail -5; echo \"EXIT=$?\"; python3 -c \"import clickhouse_connect; print('clickhouse_connect', clickhouse_connect.__version__)\" 2>&1 | tail -3"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-81ad8c7753e7e85c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 16,
"ts": "2026-11-08T02:07:33.880Z",
"turn": 1,
"output": {
"content": [
{
"text": "\n[notice] A new release of pip is available: 26.2.1 -> 26.3\n[notice] To update, run: /opt/freestyle/python/bin/python3 -m pip install --upgrade pip\nEXIT=0\nclickhouse_connect 1.12.0\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-81ad8c7753e7e85c",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The network is working. play.clickhouse.com is reachable and running ClickHouse 26.10.3.7. clickhouse-connect 1.12.0 is installed.
Alright, let me build a simple example: use clickhouse-connect (the official Python SDK) to connect to the hosted ClickHouse playground, create a table, insert data, and query it.
Note: the play.clickhouse.com playground is read-write for the play user, but databases might be shared/ephemeral. Let me test whether CREATE DATABASE / CREATE TABLE works.
First, let me confirm with a simple check whether I can create a table on play.
Actually, let's be mindful of time. Let me just try it.
Plan:
- Create a project directory structure: /sandbox/repo/clickhouse-demo/ with example.py, requirements.txt, README.md
- Example contents: connect via clickhouse-connect, create a table (events), insert rows, run analytical queries (aggregations), display results.
- Also show an alternative using the raw HTTP API (curl) — lightweight, no dependencies.
Let me test whether writes work on the playground.
cd /tmp && timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data "CREATE TABLE IF NOT EXISTS default._probe_pi (id UInt32, ts DateTime) ENGINE = MergeTree ORDER BY id" ; echo "CREATE_EXIT=$?"; timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data "INSERT INTO default._probe_pi VALUES (1, now())"; echo "INS_EXIT=$?"; timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data "SELECT count() FROM default._probe_pi"; echo "SEL_EXIT=$?"
Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default._probe_pi. (ACCESS_DENIED
Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default._probe_pi. (ACCESS_DENIED) (version 26.10.3.7 (official build)) CREATE_EXIT=0 Code: 60. DB::Exception: Table default._probe_pi does not exist. (UNKNOWN_TABLE) (version 26.10.3.7 (official build)) INS_EXIT=0 Code: 60. DB::Exception: Unknown table expression identifier 'default._probe_pi'. In scope SELECT count() FROM default._probe_pi. (UNKNOWN_TABLE) (version 26.10.3.7 (official build)) SEL_EXIT=0
Recorded tool call
{
"type": "tool_call",
"seq": 19,
"ts": "2026-11-08T02:07:38.865Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data \"CREATE TABLE IF NOT EXISTS default._probe_pi (id UInt32, ts DateTime) ENGINE = MergeTree ORDER BY id\" ; echo \"CREATE_EXIT=$?\"; timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data \"INSERT INTO default._probe_pi VALUES (1, now())\"; echo \"INS_EXIT=$?\"; timeout 30 curl -sS -m 25 'https://play.clickhouse.com/?user=play' --data \"SELECT count() FROM default._probe_pi\"; echo \"SEL_EXIT=$?\""
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-afca6f7416be9c12",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 20,
"ts": "2026-11-08T02:07:39.538Z",
"turn": 1,
"output": {
"content": [
{
"text": "Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default._probe_pi. (ACCESS_DENIED) (version 26.10.3.7 (official build))\nCREATE_EXIT=0\nCode: 60. DB::Exception: Table default._probe_pi does not exist. (UNKNOWN_TABLE) (version 26.10.3.7 (official build))\nINS_EXIT=0\nCode: 60. DB::Exception: Unknown table expression identifier 'default._probe_pi'. In scope SELECT count() FROM default._probe_pi. (UNKNOWN_TABLE) (version 26.10.3.7 (official build))\nSEL_EXIT=0\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-afca6f7416be9c12",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The public playground is read-only. So we can't create tables there. But read-only is still usable as a demo of a hosted ClickHouse API: querying the pre-installed datasets (the playground has datasets, hits, visits — actually those are on play.clickhouse.com's database datasets... let me confirm). Let me check which databases/tables exist.
For a "simple example" that includes a full write/read cycle, we need credentials for a ClickHouse Cloud instance that we don't have. Options:
- Read-only demo against the hosted playground (the real hosted ClickHouse, actual HTTP API, actual SDK) — fully doable right now.
- Write path: make the example work with ClickHouse Cloud too if credentials are provided via env vars, and demo the write path against... hmm.
Can we demo the write path against the hosted one without credentials? Options: ClickHouse Cloud's free tier requires account signup (not doable). Other hosted ClickHouse without credentials? Maybe not.
Alternative: there might be a credentials file somewhere in the sandbox. Let me check ~/.clickhouse, /etc, ~/.config.
Let me check the databases available on play, and also look for credentials files.
cd /tmp && for q in "SHOW DATABASES" "SELECT database, name, total_rows FROM system.tables WHERE database NOT IN ('system','INFORMATION_SCHEMA','information_schema') ORDER BY database,name LIMIT 40"; do echo "== $q"; timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data "$q"; done== SHOW DATABASES
== SHOW DATABASES
blogs
clickbench
coverage_ci
default
fts_benchmarks_ci
git_clickhouse
keeper_stress_tests
mgbench
system
== SELECT database, name, total_rows FROM system.tables WHERE database NOT IN ('system','INFORMATION_SCHEMA','information_schema') ORDER BY database,name LIMIT 40
blogs countries 255
blogs country_codes 279
blogs forex 11581517103
blogs forex_2020s 4023159850
blogs forex_usd 26751083
blogs noaa 1076882449
blogs noaa_v2 1076882449
blogs stop_words 140
blogs uk_codes 257
clickbench results 43349
coverage_ci coverage_data \N
default actors 33679
default benchmark_results 527476
default benchmark_runs 4251
default cell_towers 43276158
default checks \N
default checks_coverage_indirect_calls \N
default checks_coverage_inverted \N
default checks_coverage_inverted2 \N
default checks_coverage_lines \N
default checks_investigated \N
default cisco_umbrella 2565000000
default covid 8730363
default dish 428146
default dns 2191280437
default dns2 2450733093
default github_events 3173399592
default hackernews 37171166
default hackernews_changes_items 96407326
default hackernews_changes_profiles 30782346
default hackernews_history 50618636
default hackernews_top 10869689
default hits 99997497
default hits2 99997497
default hn_styles 798501
default lineorder 59986052
default loc_stats 10458728
default menu 17547
default menu_item 1334792
default menu_item_denorm 1329175
Recorded tool call
{
"type": "tool_call",
"seq": 23,
"ts": "2026-11-08T02:07:45.632Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for q in \"SHOW DATABASES\" \"SELECT database, name, total_rows FROM system.tables WHERE database NOT IN ('system','INFORMATION_SCHEMA','information_schema') ORDER BY database,name LIMIT 40\"; do echo \"== $q\"; timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data \"$q\"; done"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-8439b178a3032249",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 26,
"ts": "2026-11-08T02:07:45.980Z",
"turn": 1,
"output": {
"content": [
{
"text": "== SHOW DATABASES\nblogs\nclickbench\ncoverage_ci\ndefault\nfts_benchmarks_ci\ngit_clickhouse\nkeeper_stress_tests\nmgbench\nsystem\n== SELECT database, name, total_rows FROM system.tables WHERE database NOT IN ('system','INFORMATION_SCHEMA','information_schema') ORDER BY database,name LIMIT 40\nblogs\tcountries\t255\nblogs\tcountry_codes\t279\nblogs\tforex\t11581517103\nblogs\tforex_2020s\t4023159850\nblogs\tforex_usd\t26751083\nblogs\tnoaa\t1076882449\nblogs\tnoaa_v2\t1076882449\nblogs\tstop_words\t140\nblogs\tuk_codes\t257\nclickbench\tresults\t43349\ncoverage_ci\tcoverage_data\t\\N\ndefault\tactors\t33679\ndefault\tbenchmark_results\t527476\ndefault\tbenchmark_runs\t4251\ndefault\tcell_towers\t43276158\ndefault\tchecks\t\\N\ndefault\tchecks_coverage_indirect_calls\t\\N\ndefault\tchecks_coverage_inverted\t\\N\ndefault\tchecks_coverage_inverted2\t\\N\ndefault\tchecks_coverage_lines\t\\N\ndefault\tchecks_investigated\t\\N\ndefault\tcisco_umbrella\t2565000000\ndefault\tcovid\t8730363\ndefault\tdish\t428146\ndefault\tdns\t2191280437\ndefault\tdns2\t2450733093\ndefault\tgithub_events\t3173399592\ndefault\thackernews\t37171166\ndefault\thackernews_changes_items\t96407326\ndefault\thackernews_changes_profiles\t30782346\ndefault\thackernews_history\t50618636\ndefault\thackernews_top\t10869689\ndefault\thits\t99997497\ndefault\thits2\t99997497\ndefault\thn_styles\t798501\ndefault\tlineorder\t59986052\ndefault\tloc_stats\t10458728\ndefault\tmenu\t17547\ndefault\tmenu_item\t1334792\ndefault\tmenu_item_denorm\t1329175\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-8439b178a3032249",
"is_error": false,
"tool_kind": "shell"
}ls -la ~ 2>/dev/null; ls -la ~/.config 2>/dev/null; find / -maxdepth 4 \( -iname '*clickhouse*' \) -not -path '*/node_modules/*' -not -path '/proc/*' 2>/dev/null | head -20; echo "(end)"
total 28
total 28 drwxr-xr-x 7 root root 4096 Nov 8 02:07 . drwxr-xr-x 22 root root 4096 Sep 18 07:59 .. drwxr-xr-x 4 root root 4096 Nov 8 02:07 .cache drwxr-xr-x 3 root root 4096 Nov 8 02:07 .gauge drwxr-xr-x 3 root root 4096 Nov 8 02:07 .pi drwxrwxrwt 2 root root 4096 Sep 18 08:00 .tmp drwxr-xr-x 3 root root 4096 Sep 18 08:00 repo (end)
Recorded tool call
{
"type": "tool_call",
"seq": 24,
"ts": "2026-11-08T02:07:45.632Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls -la ~ 2>/dev/null; ls -la ~/.config 2>/dev/null; find / -maxdepth 4 \\( -iname '*clickhouse*' \\) -not -path '*/node_modules/*' -not -path '/proc/*' 2>/dev/null | head -20; echo \"(end)\""
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b894083b25606ce8",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 25,
"ts": "2026-11-08T02:07:45.632Z",
"turn": 1,
"output": {
"content": [
{
"text": "total 28\ndrwxr-xr-x 7 root root 4096 Nov 8 02:07 .\ndrwxr-xr-x 22 root root 4096 Sep 18 07:59 ..\ndrwxr-xr-x 4 root root 4096 Nov 8 02:07 .cache\ndrwxr-xr-x 3 root root 4096 Nov 8 02:07 .gauge\ndrwxr-xr-x 3 root root 4096 Nov 8 02:07 .pi\ndrwxrwxrwt 2 root root 4096 Sep 18 08:00 .tmp\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 repo\n(end)\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b894083b25606ce8",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
ClickHouse credentials aren't available. Therefore the write path against an actual hosted ClickHouse can't be demoed. But a full read/analytics demo using the official Python SDK (clickhouse-connect) against hosted ClickHouse (play.clickhouse.com — the official ClickHouse-hosted playground with real datasets) is possible.
What to build:
clickhouse-demo/example.py: SDK example against hosted ClickHouse. Supports env var overrides for connecting to your own ClickHouse Cloud instance (CLICKHOUSE_HOST/USER/PASSWORD) with a full write path (create table, insert, query), and defaults to read-only mode on the public playground.clickhouse-demo/http_api_demo.sh(or .py): raw HTTP API with curl — no dependencies.requirements.txt,README.md.
Design details: the script should be runnable without long-running commands. Keep queries small and fast. Use LIMIT and small aggregations.
Picking a dataset: default.hackernews (37M rows) — schema? Check the columns for hackernews and cell_towers. Use queries that finish fast. Also default.github_events is huge (3B rows). Let's inspect the hackernews schema and run a small aggregation to confirm latency.
Also confirm whether client.query on clickhouse_connect works against play with https and user=play (no password).
Let's write the file and test it.
First, inspect the schema.
cd /tmp && timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data "SELECT name, type FROM system.columns WHERE database='default' AND table='hackernews' FORMAT TSVWithNames"; echo "---- cell_towers"; timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data "SELECT name, type FROM system.columns WHERE database='default' AND table='cell_towers' FORMAT TSVWithNames"
name type
name type id UInt32 deleted UInt8 type Enum8(\'story\' = 1, \'comment\' = 2, \'poll\' = 3, \'pollopt\' = 4, \'job\' = 5) by LowCardinality(String) time DateTime text String dead UInt8 parent UInt32 poll UInt32 kids Array(UInt32) url String score Int32 title String parts Array(UInt32) descendants Int32 ---- cell_towers name type radio Enum8(\'\' = 0, \'CDMA\' = 1, \'GSM\' = 2, \'LTE\' = 3, \'NR\' = 4, \'UMTS\' = 5) mcc UInt16 net UInt16 area UInt16 cell UInt64 unit Int16 lon Float64 lat Float64 range UInt32 samples UInt32 changeable UInt8 created DateTime updated DateTime averageSignal UInt8
Recorded tool call
{
"type": "tool_call",
"seq": 29,
"ts": "2026-11-08T02:07:51.720Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data \"SELECT name, type FROM system.columns WHERE database='default' AND table='hackernews' FORMAT TSVWithNames\"; echo \"---- cell_towers\"; timeout 25 curl -sS -m 20 'https://play.clickhouse.com/?user=play' --data \"SELECT name, type FROM system.columns WHERE database='default' AND table='cell_towers' FORMAT TSVWithNames\""
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-80e436681957636b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 30,
"ts": "2026-11-08T02:07:52.525Z",
"turn": 1,
"output": {
"content": [
{
"text": "name\ttype\nid\tUInt32\ndeleted\tUInt8\ntype\tEnum8(\\'story\\' = 1, \\'comment\\' = 2, \\'poll\\' = 3, \\'pollopt\\' = 4, \\'job\\' = 5)\nby\tLowCardinality(String)\ntime\tDateTime\ntext\tString\ndead\tUInt8\nparent\tUInt32\npoll\tUInt32\nkids\tArray(UInt32)\nurl\tString\nscore\tInt32\ntitle\tString\nparts\tArray(UInt32)\ndescendants\tInt32\n---- cell_towers\nname\ttype\nradio\tEnum8(\\'\\' = 0, \\'CDMA\\' = 1, \\'GSM\\' = 2, \\'LTE\\' = 3, \\'NR\\' = 4, \\'UMTS\\' = 5)\nmcc\tUInt16\nnet\tUInt16\narea\tUInt16\ncell\tUInt64\nunit\tInt16\nlon\tFloat64\nlat\tFloat64\nrange\tUInt32\nsamples\tUInt32\nchangeable\tUInt8\ncreated\tDateTime\nupdated\tDateTime\naverageSignal\tUInt8\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-80e436681957636b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Alright. So, let me write the sample files.
File structure inside /sandbox/repo/clickhouse-demo/:
example.py— Main SDK sample (clickhouse-connect).- Mode A (default): hosted public playground, read-only analysis.
- Mode B: your own ClickHouse Cloud instance via environment variables → full create/insert/query lifecycle.
http_api.sh— Raw HTTP API demo using curl (no dependencies).requirements.txtREADME.md— how to run, how pricing works, troubleshooting.
Let me write example.py carefully.
Main API of clickhouse-connect 1.12:
import clickhouse_connect
client = clickhouse_connect.get_client(host=..., port=8443, username=..., password=..., secure=True, interface='https')
result = client.query('SELECT ...')
result.result_rows, result.column_names
client.command('CREATE TABLE ...')
client.insert('table', rows, column_names=[...])
Playground: host='play.clickhouse.com', port=443, username='play', password='', secure=True, interface='https'.
Note: for clickhouse-connect, the query string parameter user=play for play.clickhouse.com → username='play'.
Let me write it first and then test.
About the read-only analytics demo — 3 queries:
- Top HN posters by average score:
SELECT by AS author, count() AS stories, round(avg(score),1) AS avg_score, max(score) AS top_score
FROM default.hackernews
WHERE type = 'story' AND score > 0 AND by != ''
GROUP BY author
ORDER BY stories DESC
LIMIT 10
Hmm, with 37M rows it might be a bit slow, but ClickHouse is fast so it should finish within a few seconds.
- Yearly trend:
SELECT toYear(time) AS year, count() AS items, countIf(type='story') AS stories, round(avg(score),1) AS avg_score
FROM default.hackernews
WHERE time > '2006-01-01'
GROUP BY year ORDER BY year DESC LIMIT 8
- Something array-ish to show off ClickHouse's features: top story titles by descendants? Or geo queries with cell_towers — radio distribution by country? cell_towers doesn't have a country name, only mcc. Keep it simple:
SELECT radio, count() AS towers, round(avg(samples),0) AS avg_samples
FROM default.cell_towers GROUP BY radio ORDER BY towers DESC
Also include client.query_df to show pandas DataFrame integration? Only if pandas is installed. Let me check whether pandas is installed. clickhouse_connect might require pandas optionally. Let me check, and if pandas is available, use it in a small guarded block.
Additions: server version and elapsed time via query summaries. Show result.summary for read_rows/elapsed.
Mode B (your own Cloud instance): full lifecycle using a temp table:
client.command('CREATE DATABASE IF NOT EXISTS pi_demo')
client.command('DROP TABLE IF EXISTS pi_demo.events')
client.command('''CREATE TABLE pi_demo.events (...) ENGINE = MergeTree ORDER BY (...)''')
client.insert('pi_demo.events', rows, column_names=[...])
queries...
client.command('DROP TABLE ...')
Next, a curl script for the raw HTTP API.
Also, mention pricing in the README and reply. Let me get the pricing facts right. ClickHouse pricing:
- ClickHouse (the OSS database) is Apache-2.0 licensed and free, self-hosted.
- ClickHouse Cloud is a managed/hosted service, consumption-based pricing:
- Development tier: free tier exists — "Development" tier starts at $0, includes $25 in free monthly credits (need to verify). Actual ClickHouse Cloud pricing: tiers are Development, Production, Dedicated. Pricing is per compute unit (CU)/hour plus storage ($/GB-month), billed by the second/hour. There's a free Development tier with limited compute (a small CU included with 256?) — should be cautious here and state it generally, but accurately.
Known facts (as of 2024–2025):
- ClickHouse Cloud pricing model: pay-as-you-go based on compute units (CU) and storage (GB).
- Compute: each CU = 1 vCPU + 4GB RAM (Production). Billed per hour by the second.
- Storage: about $0.06/GB-month? Actually ClickHouse Cloud storage was $0.045/GB/month (Development?) — I'm not confident in the exact numbers.
- Development tier: $0.10/CU-hour with a minimum billing unit of 0.25 CU? Production: $0.344/CU-hour? Dedicated: custom/annual contract starting at roughly $20K/year.
- Free: "Free" Development plan gives $25/month in credits? Or rather ClickHouse Cloud offers free credits ($300 in credits for new users?) — there was a "Get started free — no credit card required" with a free Development tier.
I should be careful: state the pricing model (consumption-based: compute units per hour + storage per GB-month + separate billing for backups/egress?), mention tiers, note that the OSS is free, and recommend checking the official pricing calculator for the latest numbers. For accuracy, I can try fetching the pricing page live. Let me curl https://clickhouse.com/pricing and try to extract text. It's probably a Next.js page and JS-heavy, but there may be embedded JSON containing the pricing. Worth trying once — network is available.
Let me fetch it and grep for numbers like "$".
cd /tmp && timeout 45 curl -sSL -m 40 -A 'Mozilla/5.0' 'https://clickhouse.com/pricing' -o pricing.html; echo "EXIT=$?"; wc -c pricing.html; python3 - <<'EOF'
import re,html
s=open('/tmp/pricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script[^>]*>.*?</script>',' ',s,flags=re.S)
t=re.sub(r'<style[^>]*>.*?</style>',' ',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(re.sub(r'\s+',' ',t))
for m in re.finditer(r'.{110}\$\s?\d[\d,\.]*.{110}',t):
print('...',m.group(0).strip(),'\n')
EOFEXIT=0
EXIT=0 301348 pricing.html ... Pandas-compatible API Get started with ClickHouse Cloud for free We'll get you started on a 30 day trial and $300 credits to spend at your own pace. Start free cloud trial Solutions Use cases Real-time analytics Observabili ... ay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Li ... ation (SSO) using Google or Microsoft Social login Multi-factor authentication (MFA) See all features Storage $25.30 Per 1TB / mo Compute $0.2181 Per unit / hr Estimate your monthly cost ↓ Scale For working with production env ... based access Private networking Automatic vertical scaling Manual horizontal scaling See all features Storage $25.30 Per 1TB / mo Compute $0.2985 Per unit / hr Estimate your monthly cost ↓ Enterprise For working with productio ... on / CMEK Scheduled upgrades Consultative migrations guides HIPAA and PCI compliance See all features Storage $25.30 Per 1TB / mo Compute $0.3903 Per unit / hr Estimate your monthly cost ↓ Data transfer for public internet egr ... download the forever-free open source distribution of ClickHouse -> -> more options Compute and storage from $0.125 Per unit / hr ClickHouse Managed Postgres Fast, scalable, enterprise-grade Postgres, natively integrated with ... equently asked questions 01 How does the free trial work? Every new ClickHouse Cloud organization starts with $300 in credits to use over 30 days. The trial ends when the credits are used up or the 30 days pass, whichever co ... from Basic to Scale, or from Scale to Enterprise, at any time. Ready to try ClickHouse Cloud? Start free with $300 in credits for 30 days. Working through a complex setup or committed-spend pricing? Our sales team can help.
Recorded tool call
{
"type": "tool_call",
"seq": 33,
"ts": "2026-11-08T02:08:06.361Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && timeout 45 curl -sSL -m 40 -A 'Mozilla/5.0' 'https://clickhouse.com/pricing' -o pricing.html; echo \"EXIT=$?\"; wc -c pricing.html; python3 - <<'EOF'\nimport re,html\ns=open('/tmp/pricing.html',encoding='utf-8',errors='ignore').read()\nt=re.sub(r'<script[^>]*>.*?</script>',' ',s,flags=re.S)\nt=re.sub(r'<style[^>]*>.*?</style>',' ',t,flags=re.S)\nt=re.sub(r'<[^>]+>',' ',t)\nt=html.unescape(re.sub(r'\\s+',' ',t))\nfor m in re.finditer(r'.{110}\\$\\s?\\d[\\d,\\.]*.{110}',t):\n print('...',m.group(0).strip(),'\\n')\nEOF"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba3a8b9370c64b03",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 34,
"ts": "2026-11-08T02:08:07.299Z",
"turn": 1,
"output": {
"content": [
{
"text": "EXIT=0\n301348 pricing.html\n... Pandas-compatible API Get started with ClickHouse Cloud for free We'll get you started on a 30 day trial and $300 credits to spend at your own pace. Start free cloud trial Solutions Use cases Real-time analytics Observabili \n\n... ay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Li \n\n... ation (SSO) using Google or Microsoft Social login Multi-factor authentication (MFA) See all features Storage $25.30 Per 1TB / mo Compute $0.2181 Per unit / hr Estimate your monthly cost ↓ Scale For working with production env \n\n... based access Private networking Automatic vertical scaling Manual horizontal scaling See all features Storage $25.30 Per 1TB / mo Compute $0.2985 Per unit / hr Estimate your monthly cost ↓ Enterprise For working with productio \n\n... on / CMEK Scheduled upgrades Consultative migrations guides HIPAA and PCI compliance See all features Storage $25.30 Per 1TB / mo Compute $0.3903 Per unit / hr Estimate your monthly cost ↓ Data transfer for public internet egr \n\n... download the forever-free open source distribution of ClickHouse -> -> more options Compute and storage from $0.125 Per unit / hr ClickHouse Managed Postgres Fast, scalable, enterprise-grade Postgres, natively integrated with \n\n... equently asked questions 01 How does the free trial work? Every new ClickHouse Cloud organization starts with $300 in credits to use over 30 days. The trial ends when the credits are used up or the 30 days pass, whichever co \n\n... from Basic to Scale, or from Scale to Enterprise, at any time. Ready to try ClickHouse Cloud? Start free with $300 in credits for 30 days. Working through a complex setup or committed-spend pricing? Our sales team can help. \n\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba3a8b9370c64b03",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Excellent — live pricing data from clickhouse.com/pricing (N. Virginia us-east-1):
- Consumption-based pricing: compute and storage are metered separately, scaling to zero when idle.
- New organizations: $300 credit valid for 30 days (trial).
- Basic tier: storage $25.30 per 1TB/month, compute $0.2181/unit/hour
- Scale: compute $0.2985/unit/hour, storage $25.30/TB/month
- Enterprise: compute $0.3903/unit/hour, storage $25.30/TB/month
- Data transfer for egress to the public internet: (number needed) — let's extract that part. Also the definition of "unit" (1 unit = 1 vCPU + 4GB RAM?) and the minimum unit (Basic's minimum 0.25 units?). Let's grep the relevant sections: "unit", "Per unit", "minimum", "egress", "Data transfer".
Let's extract more context around the mentions of "Data transfer" and "unit".
cd /tmp && python3 - <<'EOF'
import re,html
s=open('/tmp/pricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script[^>]*>.*?</script>',' ',s,flags=re.S)
t=re.sub(r'<style[^>]*>.*?</style>',' ',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(re.sub(r'\s+',' ',t))
for kw in ['Data transfer','minimum','1 unit','per unit','scale to zero','scaled to zero','idle','Development','compute unit','vCPU','GB of memory','free tier','forever']:
for m in re.finditer(re.escape(kw),t,flags=re.I):
a=max(0,m.start()-160); b=min(len(t),m.end()+220)
print(f'### {kw}: ...{t[a:b]}...\n')
break
EOF### Data transfer: ...Consultative migrations guides HIPAA and PCI compliance See all features Storage $25.30 Per 1TB / mo Compute $0.3903 Per unit / hr Estimat
### Data transfer: ...Consultative migrations guides HIPAA and PCI compliance See all features Storage $25.30 Per 1TB / mo Compute $0.3903 Per unit / hr Estimate your monthly cost ↓ Data transfer for public internet egress starting at $0.1152 / GB , inter region egress starting at $0.0312 / GB . Trusted by teams at ClickPipes pricing depends on the connector type. See the ClickPipes pricing dimensions for curren... ### minimum: ...ompression Suggested configuration based on your use case Reset For real-time analytics use cases, we estimate a memory-to-storage ratio of approximately 1:25. Minimum compute per replica Maximum compute per replica Number of replicas Active hours per day Always on 24 Backups Enable Backups Backup frequency Retention period Do you know the size of your full and incremental backup? No ... ### per unit: ...n-on authentication (SSO) using Google or Microsoft Social login Multi-factor authentication (MFA) See all features Storage $25.30 Per 1TB / mo Compute $0.2181 Per unit / hr Estimate your monthly cost ↓ Scale For working with production environments, data at scale, or professional use cases. Start free trial Everything in Basic , plus: Unlimited storage Configurable memory Compute-comp... ### scaled to zero: ...Japanese Korean Chinese French Spanish Portuguese Arabic 50.8k Sign in Get Started Pricing Pay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Limited storage and memory. Start free trial Includes: Up to 1 TB storage 8-12 G... ### idle: ...ese French Spanish Portuguese Arabic 50.8k Sign in Get Started Pricing Pay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Limited storage and memory. Start free trial Includes: Up to 1 TB storage 8-12 GiB total m... ### Development: ...through a marketplace or directly from ClickHouse, and self-service customers pay monthly by credit card in USD. 08 Which plan should I start with? Basic suits development, testing and small departmental workloads on a single replica with up to 1 TB of storage. Scale is built for production: two or more availability zones, unlimited storage, autoscaling, private networking and compute-com... ### compute unit: ...ing the trial, your credits are still used first; the card is only charged once they are depleted or expire. See the billing overview for details. 02 What is a compute unit? One compute unit is 8 GiB of RAM and 2 vCPUs. Compute is metered per minute and billed per compute-unit-hour at the rate for your plan, cloud provider and region shown above. The calculator on this page applies the rig... ### vCPU: ... card is only charged once they are depleted or expire. See the billing overview for details. 02 What is a compute unit? One compute unit is 8 GiB of RAM and 2 vCPUs. Compute is metered per minute and billed per compute-unit-hour at the rate for your plan, cloud provider and region shown above. The calculator on this page applies the right rate and rounds to the sizes you can actua... ### forever: ...ClickPipes pricing dimensions for current rates. For more information about our billing and pricing please refer to our Billing & Pricing FAQ . Or download the forever-free open source distribution of ClickHouse -> -> more options Compute and storage from $0.125 Per unit / hr ClickHouse Managed Postgres Fast, scalable, enterprise-grade Postgres, natively integrated with ClickHouse. Bu...
Recorded tool call
{
"type": "tool_call",
"seq": 37,
"ts": "2026-11-08T02:08:12.188Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && python3 - <<'EOF'\nimport re,html\ns=open('/tmp/pricing.html',encoding='utf-8',errors='ignore').read()\nt=re.sub(r'<script[^>]*>.*?</script>',' ',s,flags=re.S)\nt=re.sub(r'<style[^>]*>.*?</style>',' ',t,flags=re.S)\nt=re.sub(r'<[^>]+>',' ',t)\nt=html.unescape(re.sub(r'\\s+',' ',t))\nfor kw in ['Data transfer','minimum','1 unit','per unit','scale to zero','scaled to zero','idle','Development','compute unit','vCPU','GB of memory','free tier','forever']:\n for m in re.finditer(re.escape(kw),t,flags=re.I):\n a=max(0,m.start()-160); b=min(len(t),m.end()+220)\n print(f'### {kw}: ...{t[a:b]}...\\n')\n break\nEOF"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9115d2babdd07dd8",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 38,
"ts": "2026-11-08T02:08:12.188Z",
"turn": 1,
"output": {
"content": [
{
"text": "### Data transfer: ...Consultative migrations guides HIPAA and PCI compliance See all features Storage $25.30 Per 1TB / mo Compute $0.3903 Per unit / hr Estimate your monthly cost ↓ Data transfer for public internet egress starting at $0.1152 / GB , inter region egress starting at $0.0312 / GB . Trusted by teams at ClickPipes pricing depends on the connector type. See the ClickPipes pricing dimensions for curren...\n\n### minimum: ...ompression Suggested configuration based on your use case Reset For real-time analytics use cases, we estimate a memory-to-storage ratio of approximately 1:25. Minimum compute per replica Maximum compute per replica Number of replicas Active hours per day Always on 24 Backups Enable Backups Backup frequency Retention period Do you know the size of your full and incremental backup? No ...\n\n### per unit: ...n-on authentication (SSO) using Google or Microsoft Social login Multi-factor authentication (MFA) See all features Storage $25.30 Per 1TB / mo Compute $0.2181 Per unit / hr Estimate your monthly cost ↓ Scale For working with production environments, data at scale, or professional use cases. Start free trial Everything in Basic , plus: Unlimited storage Configurable memory Compute-comp...\n\n### scaled to zero: ...Japanese Korean Chinese French Spanish Portuguese Arabic 50.8k Sign in Get Started Pricing Pay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Limited storage and memory. Start free trial Includes: Up to 1 TB storage 8-12 G...\n\n### idle: ...ese French Spanish Portuguese Arabic 50.8k Sign in Get Started Pricing Pay only for the compute and storage you use, metered separately and scaled to zero when idle. Start free with $300 in credits for 30 days. N. Virginia (us-east-1) Basic Great for testing out new ideas or starter projects. Limited storage and memory. Start free trial Includes: Up to 1 TB storage 8-12 GiB total m...\n\n### Development: ...through a marketplace or directly from ClickHouse, and self-service customers pay monthly by credit card in USD. 08 Which plan should I start with? Basic suits development, testing and small departmental workloads on a single replica with up to 1 TB of storage. Scale is built for production: two or more availability zones, unlimited storage, autoscaling, private networking and compute-com...\n\n### compute unit: ...ing the trial, your credits are still used first; the card is only charged once they are depleted or expire. See the billing overview for details. 02 What is a compute unit? One compute unit is 8 GiB of RAM and 2 vCPUs. Compute is metered per minute and billed per compute-unit-hour at the rate for your plan, cloud provider and region shown above. The calculator on this page applies the rig...\n\n### vCPU: ... card is only charged once they are depleted or expire. See the billing overview for details. 02 What is a compute unit? One compute unit is 8 GiB of RAM and 2 vCPUs. Compute is metered per minute and billed per compute-unit-hour at the rate for your plan, cloud provider and region shown above. The calculator on this page applies the right rate and rounds to the sizes you can actua...\n\n### forever: ...ClickPipes pricing dimensions for current rates. For more information about our billing and pricing please refer to our Billing & Pricing FAQ . Or download the forever-free open source distribution of ClickHouse -> -> more options Compute and storage from $0.125 Per unit / hr ClickHouse Managed Postgres Fast, scalable, enterprise-grade Postgres, natively integrated with ClickHouse. Bu...\n\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9115d2babdd07dd8",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Excellent — I've got accurate, up-to-date pricing info:
- ClickHouse OSS: permanently free (Apache-2.0).
- ClickHouse Cloud: consumption-based pricing. Compute and storage are metered separately, and idle compute scales to zero.
- 1 compute unit = 2 vCPU + 8 GiB RAM. Metered per minute, billed per compute-unit-hour.
- Rates for N. Virginia (us-east-1):
- Basic: $0.2181/unit/hour, storage $25.30 per 1TB/month, up to 1TB storage, 8–12 GiB total memory
- Scale: $0.2985/unit/hour, storage $25.30/1TB/month
- Enterprise: $0.3903/unit/hour, storage $25.30/1TB/month
- Egress: $0.1152/GB to the public internet, $0.0312/GB for inter-region.
- Trial: $300 credit over 30 days for every new organization.
- ClickPipes: pricing depends on connector type.
- Managed Postgres is a separate product: compute/storage starting at $0.125/unit/hour.
Alright, let's build the sample project. Files:
/sandbox/repo/clickhouse-demo/
- README.md
- requirements.txt
- example.py (SDK demo: defaults to the hosted playground read-only, optionally a full lifecycle via environment variables against your own instance)
- http_api.sh (raw HTTP API via curl)
First, check whether pandas is available, and after that write example.py and test it.
Also to decide: should I commit the files? The repo has git. Create the files. Committing might be nice too, but the user didn't ask. Create files, and don't commit unless asked... Actually, the repo is a workspace with an initial commit. Should I leave the changes uncommitted, or commit? Keep it simple: create the files and mention that they're uncommitted. Hmm, the common pattern in these tasks: file creation is enough. Leave git alone unless asked? Committing is low risk and useful. Ask the user... No, keep it simple — create the files, don't commit (user can review). Actually, in a fresh workspace repo, committing is often expected as "building an example." If the demo works, commit with a clear message. That's harmless and reversible.
First, check pandas.
python3 -c "import pandas; print('pandas', pandas.__version__)" 2>&1 | tail -2pandas 3.0.5
pandas 3.0.5
Recorded tool call
{
"type": "tool_call",
"seq": 41,
"ts": "2026-11-08T02:08:17.256Z",
"name": "bash",
"turn": 1,
"input": {
"command": "python3 -c \"import pandas; print('pandas', pandas.__version__)\" 2>&1 | tail -2"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-be600da3721fef92",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 42,
"ts": "2026-11-08T02:08:18.523Z",
"turn": 1,
"output": {
"content": [
{
"text": "pandas 3.0.5\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-be600da3721fef92",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
pandas is available. Next, write example.py.
Design of example.py:
#!/usr/bin/env python3
"""
Minimal ClickHouse example using the official Python SDK (clickhouse-connect).
Two modes:
1. Hosted playground (default) - ...
2. Your own instance (ClickHouse Cloud or self-hosted) - set env vars -> full write lifecycle
"""
from __future__ import annotations
import os, sys, time
from datetime import datetime, timedelta, timezone
import random
import clickhouse_connect
# --- config
HOST = os.getenv("CLICKHOUSE_HOST", "play.clickhouse.com")
PORT = int(os.getenv("CLICKHOUSE_PORT", "443"))
USER = os.getenv("CLICKHOUSE_USER", "play")
PASSWORD = os.getenv("CLICKHOUSE_PASSWORD", "")
DATABASE = os.getenv("CLICKHOUSE_DATABASE", "default")
READ_ONLY = ...
Should I add a CLICKHOUSE_MODE that derives readonly, or detect it: if host == play.clickhouse.com then it's playground read-only mode. Better: an explicit env var flag CLICKHOUSE_DEMO_WRITE=1? I'll go with this: own_instance = HOST != "play.clickhouse.com". This is clean and automatic.
Functions:
build_client()-> clickhouse_connect.get_client(...)section(title)for pretty-printingrun_query(client, sql, title)-> displays column names + rows + summary (read_rows, elapsed)analytics_demo(client)-> 3 read queries + one pandas DataFrame querywrite_demo(client)-> create DB/table, insert generated rows, query, drop
Write demo schema — a small event analytics table that shows ClickHouse idioms:
CREATE TABLE IF NOT EXISTS pi_demo.page_events
(
event_time DateTime64(3, 'UTC'),
user_id UInt64,
session_id UUID,
country LowCardinality(String),
device Enum8('desktop' = 1, 'mobile' = 2, 'tablet' = 3),
path String,
duration_ms UInt32,
tags Array(LowCardinality(String)),
payload JSON -- ? JSON type is new; risky. Either use Map(String,String) or skip.
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (country, user_id, event_time)
TTL event_time + INTERVAL 30 DAY
To avoid version issues, maybe skip the JSON type; use Map(String, String). Even on the playground version 26.x it would work, but on the user's older Cloud (e.g. 24.x) JSON is experimental. I'll go with Map(String,String) — safe.
Next, the queries:
- Per-country event count and p95 duration:
SELECT country, count() AS events, uniqExact(user_id) AS users, quantile(0.95)(duration_ms) AS p95_ms
FROM pi_demo.page_events GROUP BY country ORDER BY events DESC
- Time series:
SELECT toStartOfMinute(event_time) AS minute, count() AS events, avg(duration_ms) AS avg_ms
FROM pi_demo.page_events GROUP BY minute ORDER BY minute
- Array usage: top tags via arrayJoin:
SELECT tag, count() AS hits FROM pi_demo.page_events ARRAY JOIN tags AS tag GROUP BY tag ORDER BY hits DESC LIMIT 5
Insert: client.insert(table, rows, column_names=[...]).
Also show an idempotency hint with INSERT ... SELECT? Keep it lightweight.
Cleanup: at the end DROP TABLE (leave the database? drop both).
Note that the write demo requires a writable instance; if run against the playground, display an explanation and skip.
Error handling too: catch clickhouse_connect.driver.exceptions.ClickHouseError and show a clean message + troubleshooting tips.
For the read demo on the playground, use the default.hackernews and default.cell_towers tables. When connecting to your own instance (non-playground), the read demo tables may not exist. So: on your own instance, the write demo is the main thing; and if hackernews exists, also run the analytics queries (check via system.tables). Better structure:
analytics_demo(client, dataset_db='default'): first check table existence (query system.tables); if not present, skip with a message.
This way it works on both.
Now let's write the files.
For timing measurement, display the query elapsed from result.summary or from measured wall time. clickhouse-connect's QueryResult has .summary (a dict including elapsed and read_rows) when query settings are on... actually result.summary contains the server summary if query settings are present; simpler: measure time.perf_counter myself, and additionally get read_rows from result.summary.get('read_rows') if present.
Let me write example.py.
Also http_api.sh: curl demo — version, JSONEachRow output, parameterized query via query parameters, and note that writes are 403 on the playground. Use --data-urlencode and ?user=play. Format: JSONCompact, and CSV. Show how to switch host/user/password env vars for your own instance with HTTPS basic auth headers.
Let's write both files, then run example.py to verify.
Network is up and I have a real hosted ClickHouse to talk to. Let me scaffold the example.
File edits
1 file
Writtenclickhouse-demo/example.pyView changes
/sandbox/repo/clickhouse-demo/example.py
Written content · previous contents were not recorded
#!/usr/bin/env python3"""Minimal ClickHouse example using the official Python SDK (clickhouse-connect).Everything runs against a *hosted* ClickHouse over HTTPS. No local server, noDocker, no long-running commands.Two modes, chosen automatically from the connection settings:1. Public playground (default, zero credentials)host = play.clickhouse.com, user = play-> read-only. We run analytics against the real datasets ClickHouse hoststhere (~37M Hacker News rows, ~43M cell towers).2. Your own instance (ClickHouse Cloud trial, or any reachable server)Set CLICKHOUSE_HOST / CLICKHOUSE_USER / CLICKHOUSE_PASSWORD-> the script also does the full write lifecycle:CREATE TABLE -> INSERT -> analytical SELECTs -> DROP TABLE.Usage:python3 example.py # playground, read-only analyticsCLICKHOUSE_HOST=<host> \CLICKHOUSE_USER=<user> \CLICKHOUSE_PASSWORD=<pw> \python3 example.py # your instance, full read+writeEnv vars (all optional):CLICKHOUSE_HOST default: play.clickhouse.comCLICKHOUSE_PORT default: 443CLICKHOUSE_USER default: playCLICKHOUSE_PASSWORD default: ""CLICKHOUSE_DATABASE default: defaultCLICKHOUSE_SECURE default: true (set to "false" for plain HTTP servers)"""from __future__ import annotationsimport osimport randomimport sysimport timeimport uuidfrom datetime import datetime, timedelta, timezoneimport clickhouse_connectfrom clickhouse_connect.driver.exceptions import ClickHouseError# --------------------------------------------------------------------------# Connection settings# --------------------------------------------------------------------------PLAYGROUND_HOST = "play.clickhouse.com"HOST = os.getenv("CLICKHOUSE_HOST", PLAYGROUND_HOST)PORT = int(os.getenv("CLICKHOUSE_PORT", "443"))USER = os.getenv("CLICKHOUSE_USER", "play")PASSWORD = os.getenv("CLICKHOUSE_PASSWORD", "")DATABASE = os.getenv("CLICKHOUSE_DATABASE", "default")SECURE = os.getenv("CLICKHOUSE_SECURE", "true").lower() not in ("0", "false", "no")# The public playground grants no DDL privileges, so only attempt the write# lifecycle when we are pointed at an instance we actually own.WRITABLE = HOST != PLAYGROUND_HOSTDEMO_DB = "pi_demo"DEMO_TABLE = f"{DEMO_DB}.page_events"COUNTRIES = ["US", "DE", "GB", "JP", "BR", "IN", "FR", "CA"]DEVICES = ["desktop", "mobile", "tablet"]PATHS = ["/", "/pricing", "/docs", "/blog", "/signup", "/docs/quickstart"]TAGS = ["organic", "paid", "referral", "mobile-app", "eu", "trial", "returning"]# --------------------------------------------------------------------------# Small helpers# --------------------------------------------------------------------------def section(title: str) -> None:print(f"\n{'=' * 72}\n{title}\n{'=' * 72}")def build_client():"""Create an HTTPS client. This is the whole 'connection pool' story:clickhouse-connect keeps a pool inside the client, so create one and reuse it."""return clickhouse_connect.get_client(host=HOST,port=PORT,username=USER,password=PASSWORD,database=DATABASE,secure=SECURE,interface="https" if SECURE else "http",# Sensible defaults for a demo; tune per workload.connect_timeout=15,send_receive_timeout=60,)def show(client, sql: str, title: str, params: dict | None = None) -> None:"""Run a SELECT and print the result plus what the server told us it cost."""section(title)print(sql.strip(), "\n")started = time.perf_counter()result = client.query(sql, parameters=params)wall_ms = (time.perf_counter() - started) * 1000widths = [len(str(c)) for c in result.column_names]rows = [[("" if v is None else v) for v in row] for row in result.result_rows]for row in rows:for i, v in enumerate(row):widths[i] = max(widths[i], len(str(v)))def fmt(values):return " | ".join(str(v).ljust(w) for v, w in zip(values, widths))print(fmt(result.column_names))print("-+-".join("-" * w for w in widths))for row in rows:print(fmt(row))summary = result.summary or {}read_rows = summary.get("read_rows", "n/a")read_bytes = summary.get("read_bytes", "n/a")print(f"\n{len(rows)} rows | wall {wall_ms:.0f} ms | "f"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB"if str(read_bytes).isdigit()else f"\n{len(rows)} rows | wall {wall_ms:.0f} ms")def table_exists(client, database: str, table: str) -> bool:res = client.query("SELECT count() FROM system.tables WHERE database = {db:String} AND name = {tbl:String}",parameters={"db": database, "tbl": table},)return int(res.result_rows[0][0]) > 0# --------------------------------------------------------------------------# Part 1: read / analytics (works on any instance that has the demo datasets)# --------------------------------------------------------------------------def analytics_demo(client) -> None:if not table_exists(client, "default", "hackernews"):section("Part 1: analytics demo")print("Skipped: this instance has no `default.hackernews` dataset.")print("(The public playground hosts it; a fresh ClickHouse Cloud does not.)")returnshow(client,"""SELECTby AS author,count() AS stories,round(avg(score), 1) AS avg_score,max(score) AS best_scoreFROM default.hackernewsWHERE type = 'story' AND score > 0 AND by != ''GROUP BY authorORDER BY stories DESCLIMIT 8""","Part 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter)",)show(client,"""SELECTtoYear(time) AS year,count() AS items,countIf(type = 'story') AS stories,round(quantile(0.95)(score), 1) AS p95_scoreFROM default.hackernewsWHERE time >= toDateTime('2015-01-01')GROUP BY yearORDER BY year DESCLIMIT 6""","Part 1b: time bucketing + quantiles (quantile is approximate and very fast)",)show(client,"""SELECTradio AS technology,count() AS towers,round(avg(averageSignal), 1) AS avg_signalFROM default.cell_towersGROUP BY technologyORDER BY towers DESC""","Part 1c: Enum8 column, second dataset (~43M rows)",)# Server-side parameter binding: values are sent separately from the SQL,# so no string interpolation / injection risk.show(client,"""SELECT id, time, score, left(title, 60) AS titleFROM default.hackernewsWHERE type = 'story' AND score >= {min_score:Int32}ORDER BY score DESCLIMIT 5""","Part 1d: parameterised query (bind variables, not string building)",params={"min_score": 3000},)# Optional: pandas integration (query_df) if pandas is installed.try:import pandas # noqa: F401except ImportError:print("\n(pandas not installed - skipping query_df demo)")returnsection("Part 1e: client.query_df() -> pandas DataFrame")df = client.query_df("""SELECT toStartOfMonth(time) AS month, count() AS itemsFROM default.hackernewsWHERE time >= toDateTime('2020-01-01')GROUP BY monthORDER BY month DESCLIMIT 8""")print(df.to_string(index=False))print(f"\ndtypes:\n{df.dtypes}")# --------------------------------------------------------------------------# Part 2: write lifecycle (needs an instance where we have DDL rights)# --------------------------------------------------------------------------def generate_rows(n: int = 5_000) -> list[list]:"""Fake page-view events, newest first-ish, spread over the last hour."""rng = random.Random(42) # deterministic so the demo output is stablenow = datetime.now(timezone.utc).replace(tzinfo=None)rows = []for i in range(n):rows.append([now - timedelta(seconds=rng.randint(0, 3600)), # event_timerng.randint(1, 500), # user_iduuid.uuid4(), # session_idrng.choice(COUNTRIES), # countryrng.choice(DEVICES), # devicerng.choice(PATHS), # pathrng.randint(20, 8_000), # duration_msrng.sample(TAGS, k=rng.randint(1, 3)), # tags (Array){"plan": rng.choice(["free", "pro"]), "v": "2"}, # Map(String,String)])return rowsdef write_demo(client) -> None:section("Part 2: write lifecycle (CREATE -> INSERT -> SELECT -> DROP)")client.command(f"CREATE DATABASE IF NOT EXISTS {DEMO_DB}")client.command(f"DROP TABLE IF EXISTS {DEMO_TABLE}")client.command(f"""CREATE TABLE {DEMO_TABLE}(event_time DateTime64(3, 'UTC'),user_id UInt64,session_id UUID,country LowCardinality(String),device Enum8('desktop' = 1, 'mobile' = 2, 'tablet' = 3),path String,duration_ms UInt32,tags Array(LowCardinality(String)),attributes Map(String, String))ENGINE = MergeTreePARTITION BY toYYYYMM(event_time)ORDER BY (country, user_id, event_time)TTL toDateTime(event_time) + INTERVAL 30 DAY""")print(f"created {DEMO_TABLE} (MergeTree, partitioned by month, TTL 30 days)")rows = generate_rows()columns = ["event_time","user_id","session_id","country","device","path","duration_ms","tags","attributes",]started = time.perf_counter()client.insert(DEMO_TABLE, rows, column_names=columns)ms = (time.perf_counter() - started) * 1000print(f"inserted {len(rows):,} rows in {ms:.0f} ms "f"({len(rows) / max(ms, 1) * 1000:,.0f} rows/s) - native columnar insert, no SQL text")show(client,f"""SELECTcountry,count() AS events,uniqExact(user_id) AS users,round(quantile(0.95)(duration_ms)) AS p95_ms,round(avg(duration_ms)) AS avg_msFROM {DEMO_TABLE}GROUP BY countryORDER BY events DESC""","Part 2a: aggregate what we just wrote",)show(client,f"""SELECTtoStartOfTenMinutes(event_time) AS bucket,count() AS events,countIf(device = 'mobile') AS mobile_eventsFROM {DEMO_TABLE}GROUP BY bucketORDER BY bucket""","Part 2b: time-series rollup",)show(client,f"""SELECTtag,count() AS events,uniqExact(user_id) AS usersFROM {DEMO_TABLE}ARRAY JOIN tags AS tagGROUP BY tagORDER BY events DESCLIMIT 5""","Part 2c: ARRAY JOIN (explode an Array column into rows)",)show(client,f"""SELECTattributes['plan'] AS plan,count() AS events,round(avg(duration_ms)) AS avg_msFROM {DEMO_TABLE}GROUP BY planORDER BY events DESC""","Part 2d: read out of a Map column",)show(client,f"""SELECTname,engine,total_rows,formatReadableSize(total_bytes) AS on_disk,formatReadableSize(data_compressed_bytes) AS compressedFROM system.tablesWHERE database = '{DEMO_DB}' AND name = 'page_events'""","Part 2e: what the engine did with our data (compression ratio is the fun part)",)client.command(f"DROP TABLE IF EXISTS {DEMO_TABLE}")print(f"cleaned up: dropped {DEMO_TABLE}")# --------------------------------------------------------------------------def main() -> int:section("Connecting")print(f"host : {HOST}:{PORT} ({'https' if SECURE else 'http'})\n"f"user : {USER}\n"f"database : {DATABASE}\n"f"mode : {'read + write' if WRITABLE else 'read-only (public playground)'}")try:client = build_client()version = client.server_versiontz = client.server_tzprint(f"server : ClickHouse {version} ({tz})")except ClickHouseError as exc:print(f"\nCould not connect: {exc}", file=sys.stderr)print("\nTroubleshooting:\n"" - ClickHouse Cloud: use the HTTPS endpoint on port 8443 and the\n"" 'default' user + password from the console (SQL console -> connect).\n"" - Check CLICKHOUSE_HOST has no scheme/path, e.g. abc123.eu-west-1.aws.clickhouse.cloud\n"" - Self-hosted plain HTTP on 8123: set CLICKHOUSE_SECURE=false and CLICKHOUSE_PORT=8123",file=sys.stderr,)return 1try:analytics_demo(client)if WRITABLE:write_demo(client)else:section("Part 2: write lifecycle")print("Skipped: play.clickhouse.com is read-only ""(CREATE TABLE returns ACCESS_DENIED, code 497).\n""Point the script at your own instance to see it:\n\n"" CLICKHOUSE_HOST=<your-host> CLICKHOUSE_USER=<user> \\\n"" CLICKHOUSE_PASSWORD=<password> python3 example.py\n\n""A free ClickHouse Cloud trial ($300 of credits / 30 days) is enough.")section("Done")print("All queries ran over HTTPS against a hosted ClickHouse. No local server needed.")return 0except ClickHouseError as exc:print(f"\nClickHouse error: {exc}", file=sys.stderr)return 1finally:try:client.close()except Exception:passif __name__ == "__main__":raise SystemExit(main())
Source: write · chatcmpl-tool-9fe50ddd92a8ee0b
Recorded tool call
{
"type": "tool_call",
"seq": 51,
"ts": "2026-11-08T02:10:15.588Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/example.py",
"content": "#!/usr/bin/env python3\n\"\"\"Minimal ClickHouse example using the official Python SDK (clickhouse-connect).\n\nEverything runs against a *hosted* ClickHouse over HTTPS. No local server, no\nDocker, no long-running commands.\n\nTwo modes, chosen automatically from the connection settings:\n\n1. Public playground (default, zero credentials)\n host = play.clickhouse.com, user = play\n -> read-only. We run analytics against the real datasets ClickHouse hosts\n there (~37M Hacker News rows, ~43M cell towers).\n\n2. Your own instance (ClickHouse Cloud trial, or any reachable server)\n Set CLICKHOUSE_HOST / CLICKHOUSE_USER / CLICKHOUSE_PASSWORD\n -> the script also does the full write lifecycle:\n CREATE TABLE -> INSERT -> analytical SELECTs -> DROP TABLE.\n\nUsage:\n python3 example.py # playground, read-only analytics\n CLICKHOUSE_HOST=<host> \\\n CLICKHOUSE_USER=<user> \\\n CLICKHOUSE_PASSWORD=<pw> \\\n python3 example.py # your instance, full read+write\n\nEnv vars (all optional):\n CLICKHOUSE_HOST default: play.clickhouse.com\n CLICKHOUSE_PORT default: 443\n CLICKHOUSE_USER default: play\n CLICKHOUSE_PASSWORD default: \"\"\n CLICKHOUSE_DATABASE default: default\n CLICKHOUSE_SECURE default: true (set to \"false\" for plain HTTP servers)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport random\nimport sys\nimport time\nimport uuid\nfrom datetime import datetime, timedelta, timezone\n\nimport clickhouse_connect\nfrom clickhouse_connect.driver.exceptions import ClickHouseError\n\n# --------------------------------------------------------------------------\n# Connection settings\n# --------------------------------------------------------------------------\n\nPLAYGROUND_HOST = \"play.clickhouse.com\"\n\nHOST = os.getenv(\"CLICKHOUSE_HOST\", PLAYGROUND_HOST)\nPORT = int(os.getenv(\"CLICKHOUSE_PORT\", \"443\"))\nUSER = os.getenv(\"CLICKHOUSE_USER\", \"play\")\nPASSWORD = os.getenv(\"CLICKHOUSE_PASSWORD\", \"\")\nDATABASE = os.getenv(\"CLICKHOUSE_DATABASE\", \"default\")\nSECURE = os.getenv(\"CLICKHOUSE_SECURE\", \"true\").lower() not in (\"0\", \"false\", \"no\")\n\n# The public playground grants no DDL privileges, so only attempt the write\n# lifecycle when we are pointed at an instance we actually own.\nWRITABLE = HOST != PLAYGROUND_HOST\n\nDEMO_DB = \"pi_demo\"\nDEMO_TABLE = f\"{DEMO_DB}.page_events\"\n\nCOUNTRIES = [\"US\", \"DE\", \"GB\", \"JP\", \"BR\", \"IN\", \"FR\", \"CA\"]\nDEVICES = [\"desktop\", \"mobile\", \"tablet\"]\nPATHS = [\"/\", \"/pricing\", \"/docs\", \"/blog\", \"/signup\", \"/docs/quickstart\"]\nTAGS = [\"organic\", \"paid\", \"referral\", \"mobile-app\", \"eu\", \"trial\", \"returning\"]\n\n\n# --------------------------------------------------------------------------\n# Small helpers\n# --------------------------------------------------------------------------\n\n\ndef section(title: str) -> None:\n print(f\"\\n{'=' * 72}\\n{title}\\n{'=' * 72}\")\n\n\ndef build_client():\n \"\"\"Create an HTTPS client. This is the whole 'connection pool' story:\n clickhouse-connect keeps a pool inside the client, so create one and reuse it.\"\"\"\n return clickhouse_connect.get_client(\n host=HOST,\n port=PORT,\n username=USER,\n password=PASSWORD,\n database=DATABASE,\n secure=SECURE,\n interface=\"https\" if SECURE else \"http\",\n # Sensible defaults for a demo; tune per workload.\n connect_timeout=15,\n send_receive_timeout=60,\n )\n\n\ndef show(client, sql: str, title: str, params: dict | None = None) -> None:\n \"\"\"Run a SELECT and print the result plus what the server told us it cost.\"\"\"\n section(title)\n print(sql.strip(), \"\\n\")\n\n started = time.perf_counter()\n result = client.query(sql, parameters=params)\n wall_ms = (time.perf_counter() - started) * 1000\n\n widths = [len(str(c)) for c in result.column_names]\n rows = [[(\"\" if v is None else v) for v in row] for row in result.result_rows]\n for row in rows:\n for i, v in enumerate(row):\n widths[i] = max(widths[i], len(str(v)))\n\n def fmt(values):\n return \" | \".join(str(v).ljust(w) for v, w in zip(values, widths))\n\n print(fmt(result.column_names))\n print(\"-+-\".join(\"-\" * w for w in widths))\n for row in rows:\n print(fmt(row))\n\n summary = result.summary or {}\n read_rows = summary.get(\"read_rows\", \"n/a\")\n read_bytes = summary.get(\"read_bytes\", \"n/a\")\n print(\n f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms | \"\n f\"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB\"\n if str(read_bytes).isdigit()\n else f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n )\n\n\ndef table_exists(client, database: str, table: str) -> bool:\n res = client.query(\n \"SELECT count() FROM system.tables WHERE database = {db:String} AND name = {tbl:String}\",\n parameters={\"db\": database, \"tbl\": table},\n )\n return int(res.result_rows[0][0]) > 0\n\n\n# --------------------------------------------------------------------------\n# Part 1: read / analytics (works on any instance that has the demo datasets)\n# --------------------------------------------------------------------------\n\n\ndef analytics_demo(client) -> None:\n if not table_exists(client, \"default\", \"hackernews\"):\n section(\"Part 1: analytics demo\")\n print(\"Skipped: this instance has no `default.hackernews` dataset.\")\n print(\"(The public playground hosts it; a fresh ClickHouse Cloud does not.)\")\n return\n\n show(\n client,\n \"\"\"\n SELECT\n by AS author,\n count() AS stories,\n round(avg(score), 1) AS avg_score,\n max(score) AS best_score\n FROM default.hackernews\n WHERE type = 'story' AND score > 0 AND by != ''\n GROUP BY author\n ORDER BY stories DESC\n LIMIT 8\n \"\"\",\n \"Part 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter)\",\n )\n\n show(\n client,\n \"\"\"\n SELECT\n toYear(time) AS year,\n count() AS items,\n countIf(type = 'story') AS stories,\n round(quantile(0.95)(score), 1) AS p95_score\n FROM default.hackernews\n WHERE time >= toDateTime('2015-01-01')\n GROUP BY year\n ORDER BY year DESC\n LIMIT 6\n \"\"\",\n \"Part 1b: time bucketing + quantiles (quantile is approximate and very fast)\",\n )\n\n show(\n client,\n \"\"\"\n SELECT\n radio AS technology,\n count() AS towers,\n round(avg(averageSignal), 1) AS avg_signal\n FROM default.cell_towers\n GROUP BY technology\n ORDER BY towers DESC\n \"\"\",\n \"Part 1c: Enum8 column, second dataset (~43M rows)\",\n )\n\n # Server-side parameter binding: values are sent separately from the SQL,\n # so no string interpolation / injection risk.\n show(\n client,\n \"\"\"\n SELECT id, time, score, left(title, 60) AS title\n FROM default.hackernews\n WHERE type = 'story' AND score >= {min_score:Int32}\n ORDER BY score DESC\n LIMIT 5\n \"\"\",\n \"Part 1d: parameterised query (bind variables, not string building)\",\n params={\"min_score\": 3000},\n )\n\n # Optional: pandas integration (query_df) if pandas is installed.\n try:\n import pandas # noqa: F401\n except ImportError:\n print(\"\\n(pandas not installed - skipping query_df demo)\")\n return\n\n section(\"Part 1e: client.query_df() -> pandas DataFrame\")\n df = client.query_df(\n \"\"\"\n SELECT toStartOfMonth(time) AS month, count() AS items\n FROM default.hackernews\n WHERE time >= toDateTime('2020-01-01')\n GROUP BY month\n ORDER BY month DESC\n LIMIT 8\n \"\"\"\n )\n print(df.to_string(index=False))\n print(f\"\\ndtypes:\\n{df.dtypes}\")\n\n\n# --------------------------------------------------------------------------\n# Part 2: write lifecycle (needs an instance where we have DDL rights)\n# --------------------------------------------------------------------------\n\n\ndef generate_rows(n: int = 5_000) -> list[list]:\n \"\"\"Fake page-view events, newest first-ish, spread over the last hour.\"\"\"\n rng = random.Random(42) # deterministic so the demo output is stable\n now = datetime.now(timezone.utc).replace(tzinfo=None)\n rows = []\n for i in range(n):\n rows.append(\n [\n now - timedelta(seconds=rng.randint(0, 3600)), # event_time\n rng.randint(1, 500), # user_id\n uuid.uuid4(), # session_id\n rng.choice(COUNTRIES), # country\n rng.choice(DEVICES), # device\n rng.choice(PATHS), # path\n rng.randint(20, 8_000), # duration_ms\n rng.sample(TAGS, k=rng.randint(1, 3)), # tags (Array)\n {\"plan\": rng.choice([\"free\", \"pro\"]), \"v\": \"2\"}, # Map(String,String)\n ]\n )\n return rows\n\n\ndef write_demo(client) -> None:\n section(\"Part 2: write lifecycle (CREATE -> INSERT -> SELECT -> DROP)\")\n\n client.command(f\"CREATE DATABASE IF NOT EXISTS {DEMO_DB}\")\n client.command(f\"DROP TABLE IF EXISTS {DEMO_TABLE}\")\n client.command(\n f\"\"\"\n CREATE TABLE {DEMO_TABLE}\n (\n event_time DateTime64(3, 'UTC'),\n user_id UInt64,\n session_id UUID,\n country LowCardinality(String),\n device Enum8('desktop' = 1, 'mobile' = 2, 'tablet' = 3),\n path String,\n duration_ms UInt32,\n tags Array(LowCardinality(String)),\n attributes Map(String, String)\n )\n ENGINE = MergeTree\n PARTITION BY toYYYYMM(event_time)\n ORDER BY (country, user_id, event_time)\n TTL toDateTime(event_time) + INTERVAL 30 DAY\n \"\"\"\n )\n print(f\"created {DEMO_TABLE} (MergeTree, partitioned by month, TTL 30 days)\")\n\n rows = generate_rows()\n columns = [\n \"event_time\",\n \"user_id\",\n \"session_id\",\n \"country\",\n \"device\",\n \"path\",\n \"duration_ms\",\n \"tags\",\n \"attributes\",\n ]\n\n started = time.perf_counter()\n client.insert(DEMO_TABLE, rows, column_names=columns)\n ms = (time.perf_counter() - started) * 1000\n print(\n f\"inserted {len(rows):,} rows in {ms:.0f} ms \"\n f\"({len(rows) / max(ms, 1) * 1000:,.0f} rows/s) - native columnar insert, no SQL text\"\n )\n\n show(\n client,\n f\"\"\"\n SELECT\n country,\n count() AS events,\n uniqExact(user_id) AS users,\n round(quantile(0.95)(duration_ms)) AS p95_ms,\n round(avg(duration_ms)) AS avg_ms\n FROM {DEMO_TABLE}\n GROUP BY country\n ORDER BY events DESC\n \"\"\",\n \"Part 2a: aggregate what we just wrote\",\n )\n\n show(\n client,\n f\"\"\"\n SELECT\n toStartOfTenMinutes(event_time) AS bucket,\n count() AS events,\n countIf(device = 'mobile') AS mobile_events\n FROM {DEMO_TABLE}\n GROUP BY bucket\n ORDER BY bucket\n \"\"\",\n \"Part 2b: time-series rollup\",\n )\n\n show(\n client,\n f\"\"\"\n SELECT\n tag,\n count() AS events,\n uniqExact(user_id) AS users\n FROM {DEMO_TABLE}\n ARRAY JOIN tags AS tag\n GROUP BY tag\n ORDER BY events DESC\n LIMIT 5\n \"\"\",\n \"Part 2c: ARRAY JOIN (explode an Array column into rows)\",\n )\n\n show(\n client,\n f\"\"\"\n SELECT\n attributes['plan'] AS plan,\n count() AS events,\n round(avg(duration_ms)) AS avg_ms\n FROM {DEMO_TABLE}\n GROUP BY plan\n ORDER BY events DESC\n \"\"\",\n \"Part 2d: read out of a Map column\",\n )\n\n show(\n client,\n f\"\"\"\n SELECT\n name,\n engine,\n total_rows,\n formatReadableSize(total_bytes) AS on_disk,\n formatReadableSize(data_compressed_bytes) AS compressed\n FROM system.tables\n WHERE database = '{DEMO_DB}' AND name = 'page_events'\n \"\"\",\n \"Part 2e: what the engine did with our data (compression ratio is the fun part)\",\n )\n\n client.command(f\"DROP TABLE IF EXISTS {DEMO_TABLE}\")\n print(f\"cleaned up: dropped {DEMO_TABLE}\")\n\n\n# --------------------------------------------------------------------------\n\n\ndef main() -> int:\n section(\"Connecting\")\n print(\n f\"host : {HOST}:{PORT} ({'https' if SECURE else 'http'})\\n\"\n f\"user : {USER}\\n\"\n f\"database : {DATABASE}\\n\"\n f\"mode : {'read + write' if WRITABLE else 'read-only (public playground)'}\"\n )\n\n try:\n client = build_client()\n version = client.server_version\n tz = client.server_tz\n print(f\"server : ClickHouse {version} ({tz})\")\n except ClickHouseError as exc:\n print(f\"\\nCould not connect: {exc}\", file=sys.stderr)\n print(\n \"\\nTroubleshooting:\\n\"\n \" - ClickHouse Cloud: use the HTTPS endpoint on port 8443 and the\\n\"\n \" 'default' user + password from the console (SQL console -> connect).\\n\"\n \" - Check CLICKHOUSE_HOST has no scheme/path, e.g. abc123.eu-west-1.aws.clickhouse.cloud\\n\"\n \" - Self-hosted plain HTTP on 8123: set CLICKHOUSE_SECURE=false and CLICKHOUSE_PORT=8123\",\n file=sys.stderr,\n )\n return 1\n\n try:\n analytics_demo(client)\n\n if WRITABLE:\n write_demo(client)\n else:\n section(\"Part 2: write lifecycle\")\n print(\n \"Skipped: play.clickhouse.com is read-only \"\n \"(CREATE TABLE returns ACCESS_DENIED, code 497).\\n\"\n \"Point the script at your own instance to see it:\\n\\n\"\n \" CLICKHOUSE_HOST=<your-host> CLICKHOUSE_USER=<user> \\\\\\n\"\n \" CLICKHOUSE_PASSWORD=<password> python3 example.py\\n\\n\"\n \"A free ClickHouse Cloud trial ($300 of credits / 30 days) is enough.\"\n )\n\n section(\"Done\")\n print(\"All queries ran over HTTPS against a hosted ClickHouse. No local server needed.\")\n return 0\n except ClickHouseError as exc:\n print(f\"\\nClickHouse error: {exc}\", file=sys.stderr)\n return 1\n finally:\n try:\n client.close()\n except Exception:\n pass\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9fe50ddd92a8ee0b",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 53,
"ts": "2026-11-08T02:10:15.588Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 14999 bytes to /sandbox/repo/clickhouse-demo/example.py",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9fe50ddd92a8ee0b",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
Writtenclickhouse-demo/http_api.shView changes
/sandbox/repo/clickhouse-demo/http_api.sh
Written content · previous contents were not recorded
#!/usr/bin/env bash# Zero-dependency ClickHouse demo: the raw HTTP interface with curl.## ClickHouse speaks HTTP natively (8443/TLS or 8123/plain), so any language with# an HTTP client works. The SDKs are just convenience wrappers over this.## ./http_api.sh # public playground (read-only, no credentials)# CLICKHOUSE_HOST=<host> CLICKHOUSE_USER=<user> CLICKHOUSE_PASSWORD=<pw> ./http_api.shset -euo pipefailHOST="${CLICKHOUSE_HOST:-play.clickhouse.com}"PORT="${CLICKHOUSE_PORT:-443}"USER_NAME="${CLICKHOUSE_USER:-play}"PASSWORD="${CLICKHOUSE_PASSWORD:-}"if [[ "$HOST" == "play.clickhouse.com" ]]; thenURL="https://${HOST}:${PORT}/?user=${USER_NAME}"AUTH=()elseURL="https://${HOST}:${PORT}/"AUTH=(-H "X-ClickHouse-User: ${USER_NAME}" -H "X-ClickHouse-Key: ${PASSWORD}")firun() { # run "<label>" "<sql>" [extra curl args...]local label="$1" sql="$2"; shift 2printf '\n== %s\n' "$label"printf '%s\n\n' "$sql"curl -sS --max-time 60 "${AUTH[@]}" "$@" "$URL" --data-binary "$sql"}echo "host: ${HOST}:${PORT} user: ${USER_NAME}"run "1. server version (plain text is the default format)" "SELECT version()"run "2. JSONCompact output (?param_format=... or FORMAT clause)" \"SELECT number AS n, number * number AS squareFROM system.numbersLIMIT 5FORMAT JSONCompact"run "3. aggregation over a hosted dataset (~37M rows)" \"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_scoreFROM default.hackernewsWHERE time >= toDateTime('2019-01-01')GROUP BY yearORDER BY year DESCLIMIT 6FORMAT PrettyCompactMonoBlock"run "4. bind parameters via the query string (never interpolate user input)" \"SELECT id, score, left(title, 50) AS titleFROM default.hackernewsWHERE type = 'story' AND score >= {min_score:Int32}ORDER BY score DESCLIMIT 3FORMAT TSVWithNames" \--get --data-urlencode 'query=SELECT id, score, left(title, 50) AS titleFROM default.hackernewsWHERE type = '"'"'story'"'"' AND score >= {min_score:Int32}ORDER BY score DESCLIMIT 3FORMAT TSVWithNames' --data-urlencode 'param_min_score=4000'run "5. query metadata: how many rows/bytes the server actually read" \"SELECT count() FROM default.hackernewsSETTINGS send_progress_in_http_headers = 0FORMAT JSONCompact"run "6. system tables = free observability (slow query log, parts, merges)" \"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, queryFROM system.query_logWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'ORDER BY event_time DESCLIMIT 3FORMAT Vertical" || trueif [[ "$HOST" == "play.clickhouse.com" ]]; thenprintf '\n== 7. writes are blocked on the public playground (expected)\n'curl -sS --max-time 30 "$URL" --data-binary \'CREATE TABLE default.nope_from_demo (x UInt8) ENGINE = Memory' || trueprintf '\n\nUse your own ClickHouse Cloud instance for the write path.\n'fi
Source: write · chatcmpl-tool-ba2256f633ce7e15
Recorded tool call
{
"type": "tool_call",
"seq": 52,
"ts": "2026-11-08T02:10:15.588Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/http_api.sh",
"content": "#!/usr/bin/env bash\n# Zero-dependency ClickHouse demo: the raw HTTP interface with curl.\n#\n# ClickHouse speaks HTTP natively (8443/TLS or 8123/plain), so any language with\n# an HTTP client works. The SDKs are just convenience wrappers over this.\n#\n# ./http_api.sh # public playground (read-only, no credentials)\n# CLICKHOUSE_HOST=<host> CLICKHOUSE_USER=<user> CLICKHOUSE_PASSWORD=<pw> ./http_api.sh\nset -euo pipefail\n\nHOST=\"${CLICKHOUSE_HOST:-play.clickhouse.com}\"\nPORT=\"${CLICKHOUSE_PORT:-443}\"\nUSER_NAME=\"${CLICKHOUSE_USER:-play}\"\nPASSWORD=\"${CLICKHOUSE_PASSWORD:-}\"\n\nif [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n AUTH=()\nelse\n URL=\"https://${HOST}:${PORT}/\"\n AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\nfi\n\nrun() { # run \"<label>\" \"<sql>\" [extra curl args...]\n local label=\"$1\" sql=\"$2\"; shift 2\n printf '\\n== %s\\n' \"$label\"\n printf '%s\\n\\n' \"$sql\"\n curl -sS --max-time 60 \"${AUTH[@]}\" \"$@\" \"$URL\" --data-binary \"$sql\"\n}\n\necho \"host: ${HOST}:${PORT} user: ${USER_NAME}\"\n\nrun \"1. server version (plain text is the default format)\" \"SELECT version()\"\n\nrun \"2. JSONCompact output (?param_format=... or FORMAT clause)\" \\\n\"SELECT number AS n, number * number AS square\nFROM system.numbers\nLIMIT 5\nFORMAT JSONCompact\"\n\nrun \"3. aggregation over a hosted dataset (~37M rows)\" \\\n\"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\nFROM default.hackernews\nWHERE time >= toDateTime('2019-01-01')\nGROUP BY year\nORDER BY year DESC\nLIMIT 6\nFORMAT PrettyCompactMonoBlock\"\n\nrun \"4. bind parameters via the query string (never interpolate user input)\" \\\n\"SELECT id, score, left(title, 50) AS title\nFROM default.hackernews\nWHERE type = 'story' AND score >= {min_score:Int32}\nORDER BY score DESC\nLIMIT 3\nFORMAT TSVWithNames\" \\\n --get --data-urlencode 'query=SELECT id, score, left(title, 50) AS title\nFROM default.hackernews\nWHERE type = '\"'\"'story'\"'\"' AND score >= {min_score:Int32}\nORDER BY score DESC\nLIMIT 3\nFORMAT TSVWithNames' --data-urlencode 'param_min_score=4000'\n\nrun \"5. query metadata: how many rows/bytes the server actually read\" \\\n\"SELECT count() FROM default.hackernews\nSETTINGS send_progress_in_http_headers = 0\nFORMAT JSONCompact\"\n\nrun \"6. system tables = free observability (slow query log, parts, merges)\" \\\n\"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, query\nFROM system.query_log\nWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\nORDER BY event_time DESC\nLIMIT 3\nFORMAT Vertical\" || true\n\nif [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n printf '\\n== 7. writes are blocked on the public playground (expected)\\n'\n curl -sS --max-time 30 \"$URL\" --data-binary \\\n 'CREATE TABLE default.nope_from_demo (x UInt8) ENGINE = Memory' || true\n printf '\\n\\nUse your own ClickHouse Cloud instance for the write path.\\n'\nfi\n"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba2256f633ce7e15",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 54,
"ts": "2026-11-08T02:10:15.588Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 2910 bytes to /sandbox/repo/clickhouse-demo/http_api.sh",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-ba2256f633ce7e15",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Alright, let's test example.py. Caveat: item 4 in http_api.sh is a bit messy (the run function is passing SQL while also being given --get --data-urlencode query... as curl arguments, so both --data-binary and --get end up being included — contradictory). Let's fix http_api.sh: make item 4 a standalone curl using --get and param_min_score.
First test example.py, then fix the shell script.
cd /sandbox/repo/clickhouse-demo && chmod +x http_api.sh example.py && timeout 240 python3 example.py 2>&1 | tail -90
FROM default.hackernews
FROM default.hackernews
WHERE time >= toDateTime('2015-01-01')
GROUP BY year
ORDER BY year DESC
LIMIT 6
year | items | stories | p95_score
-----+---------+---------+----------
2023 | 2969111 | 256791 | 1.0
2022 | 4448191 | 383586 | 1.0
2021 | 4155060 | 387194 | 2.0
2020 | 3673138 | 428693 | 2.0
2019 | 3127712 | 371331 | 2.0
2018 | 2754279 | 368398 | 2.0
6 rows | wall 106 ms | server scanned 37171166 rows / 290.5 MB
========================================================================
Part 1c: Enum8 column, second dataset (~43M rows)
========================================================================
SELECT
radio AS technology,
count() AS towers,
round(avg(averageSignal), 1) AS avg_signal
FROM default.cell_towers
GROUP BY technology
ORDER BY towers DESC
technology | towers | avg_signal
-----------+----------+-----------
UMTS | 20686487 | 0.0
LTE | 12101148 | 0.0
GSM | 9931312 | 0.0
CDMA | 556344 | 0.0
NR | 867 | 0.0
5 rows | wall 97 ms | server scanned 43276158 rows / 86.6 MB
========================================================================
Part 1d: parameterised query (bind variables, not string building)
========================================================================
SELECT id, time, score, left(title, 60) AS title
FROM default.hackernews
WHERE type = 'story' AND score >= {min_score:Int32}
ORDER BY score DESC
LIMIT 5
id | time | score | title
---------+---------------------+-------+----------------------------
16582136 | 2018-03-14 03:50:30 | 6015 | Stephen Hawking has died
11116274 | 2016-02-17 08:38:37 | 5771 | A Message to Our Customers
3078128 | 2011-10-05 23:42:23 | 4338 | Steve Jobs has passed away.
37011324 | 2023-08-05 12:23:27 | 4310 | Bram Moolenaar has died
31261533 | 2022-05-04 15:06:41 | 4298 | Mechanical Watch
5 rows | wall 108 ms | server scanned 37212126 rows / 187.3 MB
========================================================================
Part 1e: client.query_df() -> pandas DataFrame
========================================================================
month items
2023-08-01 220598
2023-07-01 406196
2023-06-01 398266
2023-05-01 378428
2023-04-01 372245
2023-03-01 418710
2023-02-01 372527
2023-01-01 402141
dtypes:
month datetime64[s]
items uint64
dtype: object
========================================================================
Part 2: write lifecycle
========================================================================
Skipped: play.clickhouse.com is read-only (CREATE TABLE returns ACCESS_DENIED, code 497).
Point the script at your own instance to see it:
CLICKHOUSE_HOST=<your-host> CLICKHOUSE_USER=<user> \
CLICKHOUSE_PASSWORD=<password> python3 example.py
A free ClickHouse Cloud trial ($300 of credits / 30 days) is enough.
========================================================================
Done
========================================================================
All queries ran over HTTPS against a hosted ClickHouse. No local server needed.
Recorded tool call
{
"type": "tool_call",
"seq": 57,
"ts": "2026-11-08T02:10:19.505Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && chmod +x http_api.sh example.py && timeout 240 python3 example.py 2>&1 | tail -90"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9dd040056f7bf7b6",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 58,
"ts": "2026-11-08T02:10:21.673Z",
"turn": 1,
"output": {
"content": [
{
"text": " FROM default.hackernews\n WHERE time >= toDateTime('2015-01-01')\n GROUP BY year\n ORDER BY year DESC\n LIMIT 6 \n\nyear | items | stories | p95_score\n-----+---------+---------+----------\n2023 | 2969111 | 256791 | 1.0 \n2022 | 4448191 | 383586 | 1.0 \n2021 | 4155060 | 387194 | 2.0 \n2020 | 3673138 | 428693 | 2.0 \n2019 | 3127712 | 371331 | 2.0 \n2018 | 2754279 | 368398 | 2.0 \n\n6 rows | wall 106 ms | server scanned 37171166 rows / 290.5 MB\n\n========================================================================\nPart 1c: Enum8 column, second dataset (~43M rows)\n========================================================================\nSELECT\n radio AS technology,\n count() AS towers,\n round(avg(averageSignal), 1) AS avg_signal\n FROM default.cell_towers\n GROUP BY technology\n ORDER BY towers DESC \n\ntechnology | towers | avg_signal\n-----------+----------+-----------\nUMTS | 20686487 | 0.0 \nLTE | 12101148 | 0.0 \nGSM | 9931312 | 0.0 \nCDMA | 556344 | 0.0 \nNR | 867 | 0.0 \n\n5 rows | wall 97 ms | server scanned 43276158 rows / 86.6 MB\n\n========================================================================\nPart 1d: parameterised query (bind variables, not string building)\n========================================================================\nSELECT id, time, score, left(title, 60) AS title\n FROM default.hackernews\n WHERE type = 'story' AND score >= {min_score:Int32}\n ORDER BY score DESC\n LIMIT 5 \n\nid | time | score | title \n---------+---------------------+-------+----------------------------\n16582136 | 2018-03-14 03:50:30 | 6015 | Stephen Hawking has died \n11116274 | 2016-02-17 08:38:37 | 5771 | A Message to Our Customers \n3078128 | 2011-10-05 23:42:23 | 4338 | Steve Jobs has passed away.\n37011324 | 2023-08-05 12:23:27 | 4310 | Bram Moolenaar has died \n31261533 | 2022-05-04 15:06:41 | 4298 | Mechanical Watch \n\n5 rows | wall 108 ms | server scanned 37212126 rows / 187.3 MB\n\n========================================================================\nPart 1e: client.query_df() -> pandas DataFrame\n========================================================================\n month items\n2023-08-01 220598\n2023-07-01 406196\n2023-06-01 398266\n2023-05-01 378428\n2023-04-01 372245\n2023-03-01 418710\n2023-02-01 372527\n2023-01-01 402141\n\ndtypes:\nmonth datetime64[s]\nitems uint64\ndtype: object\n\n========================================================================\nPart 2: write lifecycle\n========================================================================\nSkipped: play.clickhouse.com is read-only (CREATE TABLE returns ACCESS_DENIED, code 497).\nPoint the script at your own instance to see it:\n\n CLICKHOUSE_HOST=<your-host> CLICKHOUSE_USER=<user> \\\n CLICKHOUSE_PASSWORD=<password> python3 example.py\n\nA free ClickHouse Cloud trial ($300 of credits / 30 days) is enough.\n\n========================================================================\nDone\n========================================================================\nAll queries ran over HTTPS against a hosted ClickHouse. No local server needed.\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9dd040056f7bf7b6",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The read path works end-to-end. Next, what needs to be verified is:
- Whether Part 1a's output (the beginning got cut off in the tail output) works — check the head portion.
- Whether write_demo's code path actually works. I don't have a writable hosted instance... but I can test the write path's SQL/schema validity without violating the "don't use local services" constraint — hmm. Running a local clickhouse-server would violate the instructions.
Alternative: use clickhouse-local? That's also a local binary and isn't installed in the first place.
Can I verify the write_demo logic without a writable server? Options:
- Static check: syntax review + verifying that all SQL is valid ClickHouse syntax. Risky spots: DateTime64(3,'UTC'), Enum8, Map, ARRAY JOIN, TTL toDateTime(event_time) + INTERVAL —
toDateTime(event_time)on DateTime64 works. ORDER BY (country, user_id, event_time) — sorting on DateTime64 is fine. - I can verify SQL parsing against the hosted playground! The playground permits SELECT and EXPLAIN. Run
EXPLAIN AST/SYNTAXorEXPLAIN PLANon the CREATE TABLE statement? EXPLAIN on DDL: does ClickHouse supportEXPLAIN AST CREATE TABLE ...? EXPLAIN AST works on any query (parses into AST) — yes, EXPLAIN AST supports arbitrary queries. AlsoEXPLAIN SYNTAX. This lets me verify syntax without executing the DDL.
Also, SELECT queries against pi_demo.page_events will fail on the playground (table doesn't exist), but I can rewrite the same SELECT text against playground tables that have equivalent columns to validate them... more simply: EXPLAIN AST on those SELECTs (parse-only, so unresolvable identifiers won't be detected).
Better approach: validate SELECT semantics by creating equivalent queries against playground tables that have similar columns? Overkill. Practical approach:
- Run
EXPLAIN AST/EXPLAIN SYNTAXon the CREATE TABLE and each Part 2 SELECT to catch syntax errors. - Also test whether client.insert's signature/column handling works — can't do that without a writable table. However, I can test
client.insertfailure handling... no, can't do it.
Actually, can I find a writable hosted ClickHouse without signing up? There's ClickHouse's "docs sample data" — not writable. Maybe there's a sandbox like https://sql.clickhouse.com? Not writable.
Hmm — what about the default database on the playground for user play... confirmed to be denied.
Do EXPLAIN-based validation, and do a Python dry-run of generate_rows() to check row/column count consistency and types (e.g., does uuid work, datetime is naive, etc.). That gives reasonable confidence. And in the README, clearly state that the write path is untested due to lack of writable credentials, and that I validated the SQL with EXPLAIN. That's honest.
Also test the following:
EXPLAIN SYNTAX CREATE TABLE ...— syntax normalization.- Verify that the Enum8 insertion mapping with string values ('desktop') works — standard and documented behavior.
- Map(String,String) insertion with a Python dict — documented.
- Array insertion with a Python list — documented.
Let me run the EXPLAIN checks and the dry-run of generate_rows.
I'll also fix item 4 of http_api.sh, then test the shell script.
First, let me look at the head of example.py's output (Part 1a).
cd /sandbox/repo/clickhouse-demo && timeout 240 python3 example.py 2>&1 | head -45
========================================================================
========================================================================
Connecting
========================================================================
host : play.clickhouse.com:443 (https)
user : play
database : default
mode : read-only (public playground)
server : ClickHouse 26.10.3.7 (UTC)
========================================================================
Part 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter)
========================================================================
SELECT
by AS author,
count() AS stories,
round(avg(score), 1) AS avg_score,
max(score) AS best_score
FROM default.hackernews
WHERE type = 'story' AND score > 0 AND by != ''
GROUP BY author
ORDER BY stories DESC
LIMIT 8
author | stories | avg_score | best_score
----------+---------+-----------+-----------
rbanffy | 28483 | 8.6 | 1793
Tomte | 21921 | 12.6 | 1336
tosh | 17551 | 17.2 | 2856
pseudolus | 15377 | 19.2 | 1336
ingve | 13697 | 29.6 | 2776
jonbaer | 13563 | 9.7 | 947
bookofjoe | 12106 | 10.5 | 1382
mooreds | 11140 | 10.1 | 1683
8 rows | wall 140 ms | server scanned 37171166 rows / 435.4 MB
========================================================================
Part 1b: time bucketing + quantiles (quantile is approximate and very fast)
========================================================================
SELECT
toYear(time) AS year,
count() AS items,
countIf(type = 'story') AS stories,
round(quantile(0.95)(score), 1) AS p95_score
Recorded tool call
{
"type": "tool_call",
"seq": 61,
"ts": "2026-11-08T02:10:39.182Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && timeout 240 python3 example.py 2>&1 | head -45"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b68a4706fe8191de",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 64,
"ts": "2026-11-08T02:10:41.366Z",
"turn": 1,
"output": {
"content": [
{
"text": "\n========================================================================\nConnecting\n========================================================================\nhost : play.clickhouse.com:443 (https)\nuser : play\ndatabase : default\nmode : read-only (public playground)\nserver : ClickHouse 26.10.3.7 (UTC)\n\n========================================================================\nPart 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter)\n========================================================================\nSELECT\n by AS author,\n count() AS stories,\n round(avg(score), 1) AS avg_score,\n max(score) AS best_score\n FROM default.hackernews\n WHERE type = 'story' AND score > 0 AND by != ''\n GROUP BY author\n ORDER BY stories DESC\n LIMIT 8 \n\nauthor | stories | avg_score | best_score\n----------+---------+-----------+-----------\nrbanffy | 28483 | 8.6 | 1793 \nTomte | 21921 | 12.6 | 1336 \ntosh | 17551 | 17.2 | 2856 \npseudolus | 15377 | 19.2 | 1336 \ningve | 13697 | 29.6 | 2776 \njonbaer | 13563 | 9.7 | 947 \nbookofjoe | 12106 | 10.5 | 1382 \nmooreds | 11140 | 10.1 | 1683 \n\n8 rows | wall 140 ms | server scanned 37171166 rows / 435.4 MB\n\n========================================================================\nPart 1b: time bucketing + quantiles (quantile is approximate and very fast)\n========================================================================\nSELECT\n toYear(time) AS year,\n count() AS items,\n countIf(type = 'story') AS stories,\n round(quantile(0.95)(score), 1) AS p95_score\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b68a4706fe8191de",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo/clickhouse-demo && python3 - <<'EOF'
# Dry-run the write-path plumbing without a writable server:
# 1) row/column shape + python types for client.insert
# 2) every Part-2 statement parsed by the real hosted server via EXPLAIN AST/SYNTAX
import example as ex, uuid, datetime
rows = ex.generate_rows(1000)
cols = ["event_time","user_id","session_id","country","device","path","duration_ms","tags","attributes"]
assert all(len(r) == len(cols) for r in rows), "column count mismatch"
print("rows:", len(rows), "| cols:", len(cols))
r = rows[0]
for c, v in zip(cols, r):
print(f" {c:12} {type(v).__name__:10} {str(v)[:48]}")
assert isinstance(r[2], uuid.UUID) and isinstance(r[0], datetime.datetime)
print("shape/type check OK")
EOFrows: 1000 | cols: 9
rows: 1000 | cols: 9
event_time datetime 2026-11-08 01:27:00.054728
user_id int 58
session_id UUID 70793702-326f-4dce-bcd3-42d46a686c88
country str US
device str tablet
path str /docs
duration_ms int 2026
tags list ['paid']
attributes dict {'plan': 'free', 'v': '2'}
shape/type check OK
Recorded tool call
{
"type": "tool_call",
"seq": 62,
"ts": "2026-11-08T02:10:39.182Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && python3 - <<'EOF'\n# Dry-run the write-path plumbing without a writable server:\n# 1) row/column shape + python types for client.insert\n# 2) every Part-2 statement parsed by the real hosted server via EXPLAIN AST/SYNTAX\nimport example as ex, uuid, datetime\nrows = ex.generate_rows(1000)\ncols = [\"event_time\",\"user_id\",\"session_id\",\"country\",\"device\",\"path\",\"duration_ms\",\"tags\",\"attributes\"]\nassert all(len(r) == len(cols) for r in rows), \"column count mismatch\"\nprint(\"rows:\", len(rows), \"| cols:\", len(cols))\nr = rows[0]\nfor c, v in zip(cols, r):\n print(f\" {c:12} {type(v).__name__:10} {str(v)[:48]}\")\nassert isinstance(r[2], uuid.UUID) and isinstance(r[0], datetime.datetime)\nprint(\"shape/type check OK\")\nEOF"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b455b76d3002f8b0",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 63,
"ts": "2026-11-08T02:10:39.182Z",
"turn": 1,
"output": {
"content": [
{
"text": "rows: 1000 | cols: 9\n event_time datetime 2026-11-08 01:27:00.054728\n user_id int 58\n session_id UUID 70793702-326f-4dce-bcd3-42d46a686c88\n country str US\n device str tablet\n path str /docs\n duration_ms int 2026\n tags list ['paid']\n attributes dict {'plan': 'free', 'v': '2'}\nshape/type check OK\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b455b76d3002f8b0",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Next, verify the Part-2 SQL statements against a hosted server using EXPLAIN AST and EXPLAIN SYNTAX. This way I can confirm the parse/normalize validity without executing any DDL.
Note: EXPLAIN SYNTAX CREATE TABLE — is this supported? EXPLAIN SYNTAX works on SELECT and some queries. Let's try both. If EXPLAIN SYNTAX doesn't support DDL, EXPLAIN AST should work.
Also verify the Part-2 SELECTs with EXPLAIN AST (parse check).
Furthermore, should I verify the analyzer's checks against CREATE TABLE with EXPLAIN PLAN? Not supported for DDL.
Another way to deeply verify the CREATE TABLE definition without DDL privileges: nothing simple. But actually — a trick: CREATE TABLE ... AS SELECT requires privileges. Hmm.
Or, can I verify the DDL semantic correctness using a playground system function? Nope, impossible.
Another trick: many ClickHouse playground-type instances allow temporary Memory tables for the play user? Already got ACCESS_DENIED on default. What about creating in my own database? CREATE DATABASE will probably also be denied. Let me quickly test CREATE DATABASE to confirm — for accurate messaging.
Let me run the EXPLAIN checks and the CREATE DATABASE investigation in a single script.
cd /sandbox/repo/clickhouse-demo && python3 - <<'EOF'
# Validate every write-path statement against the real hosted ClickHouse parser
# (EXPLAIN AST / EXPLAIN SYNTAX do not need DDL privileges).
import clickhouse_connect
from clickhouse_connect.driver.exceptions import ClickHouseError
import example as ex
client = clickhouse_connect.get_client(host='play.clickhouse.com', port=443,
username='play', password='', secure=True,
interface='https')
ddl = f"""
CREATE TABLE {ex.DEMO_TABLE}
(
event_time DateTime64(3, 'UTC'),
user_id UInt64,
session_id UUID,
country LowCardinality(String),
device Enum8('desktop' = 1, 'mobile' = 2, 'tablet' = 3),
path String,
duration_ms UInt32,
tags Array(LowCardinality(String)),
attributes Map(String, String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (country, user_id, event_time)
TTL toDateTime(event_time) + INTERVAL 30 DAY
"""
selects = {
'2a': f"SELECT country, count() AS events, uniqExact(user_id) AS users, round(quantile(0.95)(duration_ms)) AS p95_ms, round(avg(duration_ms)) AS avg_ms FROM {ex.DEMO_TABLE} GROUP BY country ORDER BY events DESC",
'2b': f"SELECT toStartOfTenMinutes(event_time) AS bucket, count() AS events, countIf(device = 'mobile') AS mobile_events FROM {ex.DEMO_TABLE} GROUP BY bucket ORDER BY bucket",
'2c': f"SELECT tag, count() AS events, uniqExact(user_id) AS users FROM {ex.DEMO_TABLE} ARRAY JOIN tags AS tag GROUP BY tag ORDER BY events DESC LIMIT 5",
'2d': f"SELECT attributes['plan'] AS plan, count() AS events, round(avg(duration_ms)) AS avg_ms FROM {ex.DEMO_TABLE} GROUP BY plan ORDER BY events DESC",
}
for kind in ('AST', 'SYNTAX'):
try:
client.command(f"EXPLAIN {kind} {ddl}")
print(f"EXPLAIN {kind} on CREATE TABLE: OK")
except ClickHouseError as e:
print(f"EXPLAIN {kind} on CREATE TABLE: {str(e)[:200]}")
for k, sql in selects.items():
try:
client.command(f"EXPLAIN AST {sql}")
print(f"EXPLAIN AST select {k}: OK")
except ClickHouseError as e:
print(f"EXPLAIN AST select {k}: FAIL {str(e)[:160]}")
# confirm the playground really has no DDL path at all (for honest messaging)
for stmt in ("CREATE DATABASE pi_demo", "CREATE TEMPORARY TABLE tmp_probe (x UInt8)"):
try:
client.command(stmt); print(f"{stmt}: ALLOWED")
except ClickHouseError as e:
print(f"{stmt}: denied -> code {str(e).split('Code: ')[1][:3] if 'Code: ' in str(e) else str(e)[:60]}")
client.close()
EOFEXPLAIN AST on CREATE TABLE: OK
EXPLAIN AST on CREATE TABLE: OK EXPLAIN SYNTAX on CREATE TABLE: OK EXPLAIN AST select 2a: OK EXPLAIN AST select 2b: OK EXPLAIN AST select 2c: OK EXPLAIN AST select 2d: OK CREATE DATABASE pi_demo: denied -> code 497 CREATE TEMPORARY TABLE tmp_probe (x UInt8): denied -> code 497
Recorded tool call
{
"type": "tool_call",
"seq": 67,
"ts": "2026-11-08T02:10:53.791Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && python3 - <<'EOF'\n# Validate every write-path statement against the real hosted ClickHouse parser\n# (EXPLAIN AST / EXPLAIN SYNTAX do not need DDL privileges).\nimport clickhouse_connect\nfrom clickhouse_connect.driver.exceptions import ClickHouseError\nimport example as ex\n\nclient = clickhouse_connect.get_client(host='play.clickhouse.com', port=443,\n username='play', password='', secure=True,\n interface='https')\nddl = f\"\"\"\nCREATE TABLE {ex.DEMO_TABLE}\n(\n event_time DateTime64(3, 'UTC'),\n user_id UInt64,\n session_id UUID,\n country LowCardinality(String),\n device Enum8('desktop' = 1, 'mobile' = 2, 'tablet' = 3),\n path String,\n duration_ms UInt32,\n tags Array(LowCardinality(String)),\n attributes Map(String, String)\n)\nENGINE = MergeTree\nPARTITION BY toYYYYMM(event_time)\nORDER BY (country, user_id, event_time)\nTTL toDateTime(event_time) + INTERVAL 30 DAY\n\"\"\"\nselects = {\n '2a': f\"SELECT country, count() AS events, uniqExact(user_id) AS users, round(quantile(0.95)(duration_ms)) AS p95_ms, round(avg(duration_ms)) AS avg_ms FROM {ex.DEMO_TABLE} GROUP BY country ORDER BY events DESC\",\n '2b': f\"SELECT toStartOfTenMinutes(event_time) AS bucket, count() AS events, countIf(device = 'mobile') AS mobile_events FROM {ex.DEMO_TABLE} GROUP BY bucket ORDER BY bucket\",\n '2c': f\"SELECT tag, count() AS events, uniqExact(user_id) AS users FROM {ex.DEMO_TABLE} ARRAY JOIN tags AS tag GROUP BY tag ORDER BY events DESC LIMIT 5\",\n '2d': f\"SELECT attributes['plan'] AS plan, count() AS events, round(avg(duration_ms)) AS avg_ms FROM {ex.DEMO_TABLE} GROUP BY plan ORDER BY events DESC\",\n}\nfor kind in ('AST', 'SYNTAX'):\n try:\n client.command(f\"EXPLAIN {kind} {ddl}\")\n print(f\"EXPLAIN {kind} on CREATE TABLE: OK\")\n except ClickHouseError as e:\n print(f\"EXPLAIN {kind} on CREATE TABLE: {str(e)[:200]}\")\nfor k, sql in selects.items():\n try:\n client.command(f\"EXPLAIN AST {sql}\")\n print(f\"EXPLAIN AST select {k}: OK\")\n except ClickHouseError as e:\n print(f\"EXPLAIN AST select {k}: FAIL {str(e)[:160]}\")\n# confirm the playground really has no DDL path at all (for honest messaging)\nfor stmt in (\"CREATE DATABASE pi_demo\", \"CREATE TEMPORARY TABLE tmp_probe (x UInt8)\"):\n try:\n client.command(stmt); print(f\"{stmt}: ALLOWED\")\n except ClickHouseError as e:\n print(f\"{stmt}: denied -> code {str(e).split('Code: ')[1][:3] if 'Code: ' in str(e) else str(e)[:60]}\")\nclient.close()\nEOF"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-96ab4ad87e4141e0",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 68,
"ts": "2026-11-08T02:10:55.999Z",
"turn": 1,
"output": {
"content": [
{
"text": "EXPLAIN AST on CREATE TABLE: OK\nEXPLAIN SYNTAX on CREATE TABLE: OK\nEXPLAIN AST select 2a: OK\nEXPLAIN AST select 2b: OK\nEXPLAIN AST select 2c: OK\nEXPLAIN AST select 2d: OK\nCREATE DATABASE pi_demo: denied -> code 497\nCREATE TEMPORARY TABLE tmp_probe (x UInt8): denied -> code 497\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-96ab4ad87e4141e0",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
All SQL parsed successfully on an actual ClickHouse 26.10 server. On the playground, DDL is completely denied (even temporary tables are not allowed).
Next, I'll fix item 4 in http_api.sh (the curl argument mixing issue) and simplify the run() helper. Rewriting http_api.sh.
Design:
- run() takes a label and SQL, POSTing via --data-binary.
- For the parameterized demo, use --get and --data-urlencode 'query=...' and 'param_min_score=4000'.
Rewriting it cleanly.
File edits
1 file
Writtenclickhouse-demo/http_api.shView changes
/sandbox/repo/clickhouse-demo/http_api.sh
Written content · previous contents were not recorded
#!/usr/bin/env bash# Zero-dependency ClickHouse demo: the raw HTTP interface with curl.## ClickHouse speaks HTTP natively (8443/TLS or 8123/plain), so any language with# an HTTP client works; the official SDKs are convenience wrappers over this.## ./http_api.sh # public playground (read-only, no credentials)# CLICKHOUSE_HOST=<host> \# CLICKHOUSE_USER=<user> \# CLICKHOUSE_PASSWORD=<pw> ./http_api.sh # your own instanceset -euo pipefailHOST="${CLICKHOUSE_HOST:-play.clickhouse.com}"PORT="${CLICKHOUSE_PORT:-443}"USER_NAME="${CLICKHOUSE_USER:-play}"PASSWORD="${CLICKHOUSE_PASSWORD:-}"if [[ "$HOST" == "play.clickhouse.com" ]]; thenURL="https://${HOST}:${PORT}/?user=${USER_NAME}"AUTH=()elseURL="https://${HOST}:${PORT}/"AUTH=(-H "X-ClickHouse-User: ${USER_NAME}" -H "X-ClickHouse-Key: ${PASSWORD}")fipost() { # post "<label>" "<sql>"printf '\n== %s\n\n%s\n\n' "$1" "$2"curl -sS --max-time 60 "${AUTH[@]}" "$URL" --data-binary "$2"}get() { # get "<label>" "<sql>" "<param_name>=<value>"printf '\n== %s\n\n%s\n\n' "$1" "$2"curl -sS --max-time 60 "${AUTH[@]}" "$URL" --get \--data-urlencode "query=$2" --data-urlencode "$3"}echo "host: ${HOST}:${PORT} user: ${USER_NAME}"post "1. server version (TabSeparated is the default output format)" \"SELECT version()"post "2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)" \"SELECT number AS n, number * number AS squareFROM system.numbersLIMIT 5FORMAT JSONCompact"post "3. aggregation over a hosted dataset (~37M rows)" \"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_scoreFROM default.hackernewsWHERE time >= toDateTime('2019-01-01')GROUP BY yearORDER BY year DESCLIMIT 6FORMAT PrettyCompactMonoBlock"get "4. bind parameters on the query string (never interpolate user input into SQL)" \"SELECT id, score, left(title, 50) AS titleFROM default.hackernewsWHERE type = 'story' AND score >= {min_score:Int32}ORDER BY score DESCLIMIT 3FORMAT TSVWithNames" \"param_min_score=4000"post "5. server-side query settings travel as query-string args / SETTINGS clause" \"SELECT count() AS rows_readFROM default.hackernewsSETTINGS max_threads = 2, max_memory_usage = 1000000000FORMAT JSONCompact"post "6. system tables are free observability (query_log, parts, merges, metrics)" \"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS queryFROM system.query_logWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'ORDER BY event_time DESCLIMIT 3FORMAT Vertical"if [[ "$HOST" == "play.clickhouse.com" ]]; thenprintf '\n== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)\n\n'curl -sS --max-time 30 "$URL" --data-binary \'CREATE TABLE default.nope_from_demo (x UInt8) ENGINE = Memory' || trueprintf '\n\nPoint CLICKHOUSE_HOST at your own ClickHouse Cloud instance for the write path.\n'fi
Source: write · chatcmpl-tool-a19a359a1eed146c
Recorded tool call
{
"type": "tool_call",
"seq": 71,
"ts": "2026-11-08T02:11:09.813Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/http_api.sh",
"content": "#!/usr/bin/env bash\n# Zero-dependency ClickHouse demo: the raw HTTP interface with curl.\n#\n# ClickHouse speaks HTTP natively (8443/TLS or 8123/plain), so any language with\n# an HTTP client works; the official SDKs are convenience wrappers over this.\n#\n# ./http_api.sh # public playground (read-only, no credentials)\n# CLICKHOUSE_HOST=<host> \\\n# CLICKHOUSE_USER=<user> \\\n# CLICKHOUSE_PASSWORD=<pw> ./http_api.sh # your own instance\nset -euo pipefail\n\nHOST=\"${CLICKHOUSE_HOST:-play.clickhouse.com}\"\nPORT=\"${CLICKHOUSE_PORT:-443}\"\nUSER_NAME=\"${CLICKHOUSE_USER:-play}\"\nPASSWORD=\"${CLICKHOUSE_PASSWORD:-}\"\n\nif [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n AUTH=()\nelse\n URL=\"https://${HOST}:${PORT}/\"\n AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\nfi\n\npost() { # post \"<label>\" \"<sql>\"\n printf '\\n== %s\\n\\n%s\\n\\n' \"$1\" \"$2\"\n curl -sS --max-time 60 \"${AUTH[@]}\" \"$URL\" --data-binary \"$2\"\n}\n\nget() { # get \"<label>\" \"<sql>\" \"<param_name>=<value>\"\n printf '\\n== %s\\n\\n%s\\n\\n' \"$1\" \"$2\"\n curl -sS --max-time 60 \"${AUTH[@]}\" \"$URL\" --get \\\n --data-urlencode \"query=$2\" --data-urlencode \"$3\"\n}\n\necho \"host: ${HOST}:${PORT} user: ${USER_NAME}\"\n\npost \"1. server version (TabSeparated is the default output format)\" \\\n\"SELECT version()\"\n\npost \"2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)\" \\\n\"SELECT number AS n, number * number AS square\nFROM system.numbers\nLIMIT 5\nFORMAT JSONCompact\"\n\npost \"3. aggregation over a hosted dataset (~37M rows)\" \\\n\"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\nFROM default.hackernews\nWHERE time >= toDateTime('2019-01-01')\nGROUP BY year\nORDER BY year DESC\nLIMIT 6\nFORMAT PrettyCompactMonoBlock\"\n\nget \"4. bind parameters on the query string (never interpolate user input into SQL)\" \\\n\"SELECT id, score, left(title, 50) AS title\nFROM default.hackernews\nWHERE type = 'story' AND score >= {min_score:Int32}\nORDER BY score DESC\nLIMIT 3\nFORMAT TSVWithNames\" \\\n\"param_min_score=4000\"\n\npost \"5. server-side query settings travel as query-string args / SETTINGS clause\" \\\n\"SELECT count() AS rows_read\nFROM default.hackernews\nSETTINGS max_threads = 2, max_memory_usage = 1000000000\nFORMAT JSONCompact\"\n\npost \"6. system tables are free observability (query_log, parts, merges, metrics)\" \\\n\"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query\nFROM system.query_log\nWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\nORDER BY event_time DESC\nLIMIT 3\nFORMAT Vertical\"\n\nif [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n printf '\\n== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)\\n\\n'\n curl -sS --max-time 30 \"$URL\" --data-binary \\\n 'CREATE TABLE default.nope_from_demo (x UInt8) ENGINE = Memory' || true\n printf '\\n\\nPoint CLICKHOUSE_HOST at your own ClickHouse Cloud instance for the write path.\\n'\nfi\n"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-a19a359a1eed146c",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 73,
"ts": "2026-11-08T02:11:09.813Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 2999 bytes to /sandbox/repo/clickhouse-demo/http_api.sh",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-a19a359a1eed146c",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
Writtenclickhouse-demo/requirements.txtView changes
/sandbox/repo/clickhouse-demo/requirements.txt
Written content · previous contents were not recorded
clickhouse-connect>=0.7.0
Source: write · chatcmpl-tool-9e7f67ebf0ebb453
Recorded tool call
{
"type": "tool_call",
"seq": 72,
"ts": "2026-11-08T02:11:09.813Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/requirements.txt",
"content": "clickhouse-connect>=0.7.0\n"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9e7f67ebf0ebb453",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 74,
"ts": "2026-11-08T02:11:09.813Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 26 bytes to /sandbox/repo/clickhouse-demo/requirements.txt",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9e7f67ebf0ebb453",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Wait a sec, pandas 3.0.5 is installed but does clickhouse-connect work? Yes, query_df worked.
Next, test http_api.sh. Note: item 5 which includes SETTINGS — no problem. Item 6 uses system.query_log — the playground user may not have access to query_log. Let's check. If access is denied, it'll error out and set -euo pipefail will halt the script. Let's test it and adjust (add || true, or pick a different system table).
cd /sandbox/repo/clickhouse-demo && chmod +x http_api.sh && timeout 180 ./http_api.sh 2>&1 | tail -60; echo "SCRIPT_EXIT=${PIPESTATUS[0]}"Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.numbers. (ACCESS_DENIED) (versio
Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.numbers. (ACCESS_DENIED) (version 26.10.3.7 (official build))
== 3. aggregation over a hosted dataset (~37M rows)
SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score
FROM default.hackernews
WHERE time >= toDateTime('2019-01-01')
GROUP BY year
ORDER BY year DESC
LIMIT 6
FORMAT PrettyCompactMonoBlock
┌─year─┬───items─┬─avg_score─┐
1. │ 2023 │ 2969111 │ 1.5 │
2. │ 2022 │ 4448191 │ 1.5 │
3. │ 2021 │ 4155060 │ 1.5 │
4. │ 2020 │ 3673138 │ 1.7 │
5. │ 2019 │ 3127712 │ 1.8 │
└──────┴─────────┴───────────┘
== 4. bind parameters on the query string (never interpolate user input into SQL)
SELECT id, score, left(title, 50) AS title
FROM default.hackernews
WHERE type = 'story' AND score >= {min_score:Int32}
ORDER BY score DESC
LIMIT 3
FORMAT TSVWithNames
id score title
16582136 6015 Stephen Hawking has died
11116274 5771 A Message to Our Customers
3078128 4338 Steve Jobs has passed away.
== 5. server-side query settings travel as query-string args / SETTINGS clause
SELECT count() AS rows_read
FROM default.hackernews
SETTINGS max_threads = 2, max_memory_usage = 1000000000
FORMAT JSONCompact
Code: 164. DB::Exception: Cannot modify 'max_threads' setting in readonly mode. (READONLY) (version 26.10.3.7 (official build))
== 6. system tables are free observability (query_log, parts, merges, metrics)
SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query
FROM system.query_log
WHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'
ORDER BY event_time DESC
LIMIT 3
FORMAT Vertical
Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.query_log. (ACCESS_DENIED) (version 26.10.3.7 (official build))
== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)
Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default.nope_from_demo. (ACCESS_DENIED) (version 26.10.3.7 (official build))
Point CLICKHOUSE_HOST at your own ClickHouse Cloud instance for the write path.
SCRIPT_EXIT=0
Recorded tool call
{
"type": "tool_call",
"seq": 77,
"ts": "2026-11-08T02:11:13.716Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && chmod +x http_api.sh && timeout 180 ./http_api.sh 2>&1 | tail -60; echo \"SCRIPT_EXIT=${PIPESTATUS[0]}\""
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b26def072058ed48",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 78,
"ts": "2026-11-08T02:11:16.279Z",
"turn": 1,
"output": {
"content": [
{
"text": "Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.numbers. (ACCESS_DENIED) (version 26.10.3.7 (official build))\n\n== 3. aggregation over a hosted dataset (~37M rows)\n\nSELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\nFROM default.hackernews\nWHERE time >= toDateTime('2019-01-01')\nGROUP BY year\nORDER BY year DESC\nLIMIT 6\nFORMAT PrettyCompactMonoBlock\n\n ┌─year─┬───items─┬─avg_score─┐\n1. │ 2023 │ 2969111 │ 1.5 │\n2. │ 2022 │ 4448191 │ 1.5 │\n3. │ 2021 │ 4155060 │ 1.5 │\n4. │ 2020 │ 3673138 │ 1.7 │\n5. │ 2019 │ 3127712 │ 1.8 │\n └──────┴─────────┴───────────┘\n\n== 4. bind parameters on the query string (never interpolate user input into SQL)\n\nSELECT id, score, left(title, 50) AS title\nFROM default.hackernews\nWHERE type = 'story' AND score >= {min_score:Int32}\nORDER BY score DESC\nLIMIT 3\nFORMAT TSVWithNames\n\nid\tscore\ttitle\n16582136\t6015\tStephen Hawking has died\n11116274\t5771\tA Message to Our Customers\n3078128\t4338\tSteve Jobs has passed away.\n\n== 5. server-side query settings travel as query-string args / SETTINGS clause\n\nSELECT count() AS rows_read\nFROM default.hackernews\nSETTINGS max_threads = 2, max_memory_usage = 1000000000\nFORMAT JSONCompact\n\nCode: 164. DB::Exception: Cannot modify 'max_threads' setting in readonly mode. (READONLY) (version 26.10.3.7 (official build))\n\n== 6. system tables are free observability (query_log, parts, merges, metrics)\n\nSELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query\nFROM system.query_log\nWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\nORDER BY event_time DESC\nLIMIT 3\nFORMAT Vertical\n\nCode: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.query_log. (ACCESS_DENIED) (version 26.10.3.7 (official build))\n\n== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)\n\nCode: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default.nope_from_demo. (ACCESS_DENIED) (version 26.10.3.7 (official build))\n\n\nPoint CLICKHOUSE_HOST at your own ClickHouse Cloud instance for the write path.\nSCRIPT_EXIT=0\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b26def072058ed48",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The playground is a readonly=2 style restricted user: system.numbers is denied, settings changes are denied, system.query_log is denied. Need to fix items 2, 5, and 6 so the script works cleanly in the playground too.
Replacements:
- Item 2 (JSON format): use an actual dataset instead of system.numbers. Example:
SELECT id, score, left(title,40) AS title FROM default.hackernews WHERE score > 3000 ORDER BY score DESC LIMIT 3 FORMAT JSONCompact. Or generate numbers without using system.numbers:SELECT arrayJoin([1,2,3,4,5]) AS n— works without system tables. To demonstrate a generator function, usenumbers… simplest isSELECT number...isn't allowed. Use arrayJoin or a dataset. I'll go with arrayJoin — shows off ClickHouse's array functions. - Item 5 (settings): in playground readonly, settings cannot be changed. Two options: show settings usage but tolerate failure with a clear note; or use settings allowed even in readonly (readonly=2 allows some settings? Apparently max_threads is denied). Better approach: change item 5 to a demo of query string parameters that are allowed: e.g.
?default_format=CSVas a URL parameter (this is an HTTP-level parameter, not a server setting) — this works:curl "$URL&default_format=CSV". This is a good HTTP API lesson. Add a note that server-side SETTINGS require a non-readonly user, and on your own instance you'd useSETTINGS .... - Item 6 (system tables): playground denies query_log. Which system tables can play read? Previously
system.tablesandsystem.columnsworked. So usesystem.tables/system.columnsfor the observability demo, and mention query_log/parts for your own instance. Verify thatsystem.partsandsystem.metricsare accessible? Let's test a few: system.tables (works), system.columns (works), system.parts, system.events, system.metrics, system.settings, system.functions, system.formats.
Let's test which ones are readable, then rewrite items 2, 5, 6 accordingly. Additionally add an || true guard so the script doesn't die under pipefail… actually, with set -e, if curl succeeds (exit code 0) and returns error text, the script continues. The failure above returned HTTP 200 with error text? curl's exit code was 0, so the script ran all the way through (SCRIPT_EXIT=0). Still, make sure everything succeeds cleanly.
Let's verify access to the system tables.
cd /tmp && for t in tables columns parts events metrics settings functions formats numbers one clusters processes query_log; do printf '%-12s ' "$t"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play' --data "SELECT count() FROM system.$t" 2>&1 | head -1; done; echo "--- format via url param:"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play&default_format=CSV' --data "SELECT 1 AS a, 'x' AS b" | head -3; echo "--- arrayJoin:"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play&default_format=JSONCompact' --data "SELECT arrayJoin([1,2,3]) AS n, n*n AS square" | head -8
tables 95
tables 95
columns 3886
parts Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.parts. (ACCESS_DENIED) (version 26.10.3.7 (official build))
events 386
metrics 575
settings 1956
functions 2008
formats 116
numbers Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.numbers. (ACCESS_DENIED) (version 26.10.3.7 (official build))
one 1
clusters Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.clusters. (ACCESS_DENIED) (version 26.10.3.7 (official build))
processes Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.processes. (ACCESS_DENIED) (version 26.10.3.7 (official build))
query_log Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.query_log. (ACCESS_DENIED) (version 26.10.3.7 (official build))
--- format via url param:
1,"x"
--- arrayJoin:
{
"meta":
[
{
"name": "n",
"type": "UInt8"
},
{
Recorded tool call
{
"type": "tool_call",
"seq": 81,
"ts": "2026-11-08T02:11:28.543Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for t in tables columns parts events metrics settings functions formats numbers one clusters processes query_log; do printf '%-12s ' \"$t\"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play' --data \"SELECT count() FROM system.$t\" 2>&1 | head -1; done; echo \"--- format via url param:\"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play&default_format=CSV' --data \"SELECT 1 AS a, 'x' AS b\" | head -3; echo \"--- arrayJoin:\"; timeout 20 curl -sS -m 15 'https://play.clickhouse.com/?user=play&default_format=JSONCompact' --data \"SELECT arrayJoin([1,2,3]) AS n, n*n AS square\" | head -8"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9eb98f866fb5237a",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 82,
"ts": "2026-11-08T02:11:33.090Z",
"turn": 1,
"output": {
"content": [
{
"text": "tables 95\ncolumns 3886\nparts Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.parts. (ACCESS_DENIED) (version 26.10.3.7 (official build))\nevents 386\nmetrics 575\nsettings 1956\nfunctions 2008\nformats 116\nnumbers Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.numbers. (ACCESS_DENIED) (version 26.10.3.7 (official build))\none 1\nclusters Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.clusters. (ACCESS_DENIED) (version 26.10.3.7 (official build))\nprocesses Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.processes. (ACCESS_DENIED) (version 26.10.3.7 (official build))\nquery_log Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant SELECT ON system.query_log. (ACCESS_DENIED) (version 26.10.3.7 (official build))\n--- format via url param:\n1,\"x\"\n--- arrayJoin:\n{\n\t\"meta\":\n\t[\n\t\t{\n\t\t\t\"name\": \"n\",\n\t\t\t\"type\": \"UInt8\"\n\t\t},\n\t\t{\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9eb98f866fb5237a",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Next, rewrite items 2, 5, and 6 of http_api.sh using playground-compatible queries, and add a note about which ones require one's own instance.
Item 2: arrayJoin + default_format=JSONCompact via URL parameter? Should item 2 stay as a POST using FORMAT JSONCompact with arrayJoin?
Item 5: default_format=CSV via URL parameter (HTTP-level), plus a commented-out note showing a SETTINGS example for one's own instance.
Item 6: Observability of system tables using system.tables + system.events/metrics (permitted targets). Display the top largest tables by total_bytes and some metrics. On one's own instance, mention system.query_log / system.parts.
Alright, let's write it.
File edits
1 file
Modifiedclickhouse-demo/http_api.sh+20−14View changes
/sandbox/repo/clickhouse-demo/http_api.sh
Replacement excerpt
post "2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)" \-"SELECT number AS n, number * number AS square-FROM system.numbers-LIMIT 5+"SELECT arrayJoin([1, 2, 3, 4, 5]) AS n, n * n AS squareFORMAT JSONCompact"
Replacement excerpt
-post "5. server-side query settings travel as query-string args / SETTINGS clause" \-"SELECT count() AS rows_read+printf '\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\n\n'+curl -sS --max-time 60 "${AUTH[@]}" "${URL}&default_format=CSV" --data-binary \+"SELECT id, score, left(title, 40) AS titleFROM default.hackernews-SETTINGS max_threads = 2, max_memory_usage = 1000000000-FORMAT JSONCompact"+WHERE score > 4000+ORDER BY score DESC+LIMIT 3"+# On an instance you own (not readonly) you can also tune the server per query:+# ... FROM default.hackernews SETTINGS max_threads = 2, max_memory_usage = 1000000000-post "6. system tables are free observability (query_log, parts, merges, metrics)" \-"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query-FROM system.query_log-WHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'-ORDER BY event_time DESC-LIMIT 3-FORMAT Vertical"+post "6. system tables = free observability (here: what is on this server)" \+"SELECT database, count() AS tables, formatReadableSize(sum(total_bytes)) AS on_disk,+ sum(total_rows) AS rows+FROM system.tables+WHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')+GROUP BY database+ORDER BY sum(total_bytes) DESC+LIMIT 5+FORMAT PrettyCompactMonoBlock"+# On your own instance the interesting ones are system.query_log (slow queries),+# system.parts (data layout), system.merges and system.metrics.
Source: edit · chatcmpl-tool-a26047184dc10592
Recorded tool call
{
"type": "tool_call",
"seq": 85,
"ts": "2026-11-08T02:11:44.546Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/http_api.sh",
"edits": [
{
"newText": "post \"2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)\" \\\n\"SELECT arrayJoin([1, 2, 3, 4, 5]) AS n, n * n AS square\nFORMAT JSONCompact\"",
"oldText": "post \"2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)\" \\\n\"SELECT number AS n, number * number AS square\nFROM system.numbers\nLIMIT 5\nFORMAT JSONCompact\""
},
{
"newText": "printf '\\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\\n\\n'\ncurl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\\n\"SELECT id, score, left(title, 40) AS title\nFROM default.hackernews\nWHERE score > 4000\nORDER BY score DESC\nLIMIT 3\"\n# On an instance you own (not readonly) you can also tune the server per query:\n# ... FROM default.hackernews SETTINGS max_threads = 2, max_memory_usage = 1000000000\n\npost \"6. system tables = free observability (here: what is on this server)\" \\\n\"SELECT database, count() AS tables, formatReadableSize(sum(total_bytes)) AS on_disk,\n sum(total_rows) AS rows\nFROM system.tables\nWHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')\nGROUP BY database\nORDER BY sum(total_bytes) DESC\nLIMIT 5\nFORMAT PrettyCompactMonoBlock\"\n# On your own instance the interesting ones are system.query_log (slow queries),\n# system.parts (data layout), system.merges and system.metrics.",
"oldText": "post \"5. server-side query settings travel as query-string args / SETTINGS clause\" \\\n\"SELECT count() AS rows_read\nFROM default.hackernews\nSETTINGS max_threads = 2, max_memory_usage = 1000000000\nFORMAT JSONCompact\"\n\npost \"6. system tables are free observability (query_log, parts, merges, metrics)\" \\\n\"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query\nFROM system.query_log\nWHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\nORDER BY event_time DESC\nLIMIT 3\nFORMAT Vertical\""
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-a26047184dc10592",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 86,
"ts": "2026-11-08T02:11:44.546Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 2 block(s) in /sandbox/repo/clickhouse-demo/http_api.sh.",
"type": "text"
}
],
"details": {
"diff": " ...\n 39 post \"1. server version (TabSeparated is the default output format)\" \\\n 40 \"SELECT version()\"\n 41 \n 42 post \"2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)\" \\\n-43 \"SELECT number AS n, number * number AS square\n-44 FROM system.numbers\n-45 LIMIT 5\n+43 \"SELECT arrayJoin([1, 2, 3, 4, 5]) AS n, n * n AS square\n 46 FORMAT JSONCompact\"\n 47 \n 48 post \"3. aggregation over a hosted dataset (~37M rows)\" \\\n 49 \"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\n ...\n 62 LIMIT 3\n 63 FORMAT TSVWithNames\" \\\n 64 \"param_min_score=4000\"\n 65 \n-66 post \"5. server-side query settings travel as query-string args / SETTINGS clause\" \\\n-67 \"SELECT count() AS rows_read\n+64 printf '\\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\\n\\n'\n+65 curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\\n+66 \"SELECT id, score, left(title, 40) AS title\n 68 FROM default.hackernews\n-69 SETTINGS max_threads = 2, max_memory_usage = 1000000000\n-70 FORMAT JSONCompact\"\n+68 WHERE score > 4000\n+69 ORDER BY score DESC\n+70 LIMIT 3\"\n+71 # On an instance you own (not readonly) you can also tune the server per query:\n+72 # ... FROM default.hackernews SETTINGS max_threads = 2, max_memory_usage = 1000000000\n 71 \n-72 post \"6. system tables are free observability (query_log, parts, merges, metrics)\" \\\n-73 \"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query\n-74 FROM system.query_log\n-75 WHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\n-76 ORDER BY event_time DESC\n-77 LIMIT 3\n-78 FORMAT Vertical\"\n+74 post \"6. system tables = free observability (here: what is on this server)\" \\\n+75 \"SELECT database, count() AS tables, formatReadableSize(sum(total_bytes)) AS on_disk,\n+76 sum(total_rows) AS rows\n+77 FROM system.tables\n+78 WHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')\n+79 GROUP BY database\n+80 ORDER BY sum(total_bytes) DESC\n+81 LIMIT 5\n+82 FORMAT PrettyCompactMonoBlock\"\n+83 # On your own instance the interesting ones are system.query_log (slow queries),\n+84 # system.parts (data layout), system.merges and system.metrics.\n 79 \n 80 if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n 81 printf '\\n== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)\\n\\n'\n 82 curl -sS --max-time 30 \"$URL\" --data-binary \\\n ...",
"patch": "--- /sandbox/repo/clickhouse-demo/http_api.sh\n+++ /sandbox/repo/clickhouse-demo/http_api.sh\n@@ -39,11 +39,9 @@\n post \"1. server version (TabSeparated is the default output format)\" \\\n \"SELECT version()\"\n \n post \"2. ask for JSON with a FORMAT clause (JSONCompact, JSONEachRow, CSV, Parquet...)\" \\\n-\"SELECT number AS n, number * number AS square\n-FROM system.numbers\n-LIMIT 5\n+\"SELECT arrayJoin([1, 2, 3, 4, 5]) AS n, n * n AS square\n FORMAT JSONCompact\"\n \n post \"3. aggregation over a hosted dataset (~37M rows)\" \\\n \"SELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\n@@ -62,21 +60,29 @@\n LIMIT 3\n FORMAT TSVWithNames\" \\\n \"param_min_score=4000\"\n \n-post \"5. server-side query settings travel as query-string args / SETTINGS clause\" \\\n-\"SELECT count() AS rows_read\n+printf '\\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\\n\\n'\n+curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\\n+\"SELECT id, score, left(title, 40) AS title\n FROM default.hackernews\n-SETTINGS max_threads = 2, max_memory_usage = 1000000000\n-FORMAT JSONCompact\"\n+WHERE score > 4000\n+ORDER BY score DESC\n+LIMIT 3\"\n+# On an instance you own (not readonly) you can also tune the server per query:\n+# ... FROM default.hackernews SETTINGS max_threads = 2, max_memory_usage = 1000000000\n \n-post \"6. system tables are free observability (query_log, parts, merges, metrics)\" \\\n-\"SELECT query_duration_ms, read_rows, formatReadableSize(read_bytes) AS read, left(query, 60) AS query\n-FROM system.query_log\n-WHERE type = 'QueryFinish' AND query NOT LIKE '%query_log%'\n-ORDER BY event_time DESC\n-LIMIT 3\n-FORMAT Vertical\"\n+post \"6. system tables = free observability (here: what is on this server)\" \\\n+\"SELECT database, count() AS tables, formatReadableSize(sum(total_bytes)) AS on_disk,\n+ sum(total_rows) AS rows\n+FROM system.tables\n+WHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')\n+GROUP BY database\n+ORDER BY sum(total_bytes) DESC\n+LIMIT 5\n+FORMAT PrettyCompactMonoBlock\"\n+# On your own instance the interesting ones are system.query_log (slow queries),\n+# system.parts (data layout), system.merges and system.metrics.\n \n if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n printf '\\n== 7. writes are blocked on the public playground (expected: ACCESS_DENIED 497)\\n\\n'\n curl -sS --max-time 30 \"$URL\" --data-binary \\\n",
"firstChangedLine": 43
}
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-a26047184dc10592",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Wait: ${URL}&default_format=CSV — in the case of the playground, the URL already has ?user=play attached, so appending &default_format=CSV will work. However, for my own instance, the URL is https://host:port/ without a query string, so &default_format=CSV will be incorrect. Fix: conditionally construct FORMAT_PARAM.
Let me fix this with a variable: if the URL contains '?', then SEP='&'; otherwise SEP='?'.
File edits
1 file
Modifiedclickhouse-demo/http_api.sh+3−1View changes
/sandbox/repo/clickhouse-demo/http_api.sh
Replacement excerpt
if [[ "$HOST" == "play.clickhouse.com" ]]; thenURL="https://${HOST}:${PORT}/?user=${USER_NAME}"AUTH=()+ SEP="&" # URL already carries a query stringelseURL="https://${HOST}:${PORT}/"AUTH=(-H "X-ClickHouse-User: ${USER_NAME}" -H "X-ClickHouse-Key: ${PASSWORD}")+ SEP="?"fi
Replacement excerpt
-curl -sS --max-time 60 "${AUTH[@]}" "${URL}&default_format=CSV" --data-binary \+curl -sS --max-time 60 "${AUTH[@]}" "${URL}${SEP}default_format=CSV" --data-binary \
Source: edit · chatcmpl-tool-b905ae1fd1c0a06b
Recorded tool call
{
"type": "tool_call",
"seq": 89,
"ts": "2026-11-08T02:11:49.629Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/http_api.sh",
"edits": [
{
"newText": "if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n AUTH=()\n SEP=\"&\" # URL already carries a query string\nelse\n URL=\"https://${HOST}:${PORT}/\"\n AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\n SEP=\"?\"\nfi",
"oldText": "if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n AUTH=()\nelse\n URL=\"https://${HOST}:${PORT}/\"\n AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\nfi"
},
{
"newText": "curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}${SEP}default_format=CSV\" --data-binary \\",
"oldText": "curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b905ae1fd1c0a06b",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 90,
"ts": "2026-11-08T02:11:49.629Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 2 block(s) in /sandbox/repo/clickhouse-demo/http_api.sh.",
"type": "text"
}
],
"details": {
"diff": " ...\n 17 \n 18 if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n 19 URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n 20 AUTH=()\n+21 SEP=\"&\" # URL already carries a query string\n 21 else\n 22 URL=\"https://${HOST}:${PORT}/\"\n 23 AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\n+25 SEP=\"?\"\n 24 fi\n 25 \n 26 post() { # post \"<label>\" \"<sql>\"\n 27 printf '\\n== %s\\n\\n%s\\n\\n' \"$1\" \"$2\"\n ...\n 61 FORMAT TSVWithNames\" \\\n 62 \"param_min_score=4000\"\n 63 \n 64 printf '\\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\\n\\n'\n-65 curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\\n+67 curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}${SEP}default_format=CSV\" --data-binary \\\n 66 \"SELECT id, score, left(title, 40) AS title\n 67 FROM default.hackernews\n 68 WHERE score > 4000\n 69 ORDER BY score DESC\n ...",
"patch": "--- /sandbox/repo/clickhouse-demo/http_api.sh\n+++ /sandbox/repo/clickhouse-demo/http_api.sh\n@@ -17,11 +17,13 @@\n \n if [[ \"$HOST\" == \"play.clickhouse.com\" ]]; then\n URL=\"https://${HOST}:${PORT}/?user=${USER_NAME}\"\n AUTH=()\n+ SEP=\"&\" # URL already carries a query string\n else\n URL=\"https://${HOST}:${PORT}/\"\n AUTH=(-H \"X-ClickHouse-User: ${USER_NAME}\" -H \"X-ClickHouse-Key: ${PASSWORD}\")\n+ SEP=\"?\"\n fi\n \n post() { # post \"<label>\" \"<sql>\"\n printf '\\n== %s\\n\\n%s\\n\\n' \"$1\" \"$2\"\n@@ -61,9 +63,9 @@\n FORMAT TSVWithNames\" \\\n \"param_min_score=4000\"\n \n printf '\\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\\n\\n'\n-curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}&default_format=CSV\" --data-binary \\\n+curl -sS --max-time 60 \"${AUTH[@]}\" \"${URL}${SEP}default_format=CSV\" --data-binary \\\n \"SELECT id, score, left(title, 40) AS title\n FROM default.hackernews\n WHERE score > 4000\n ORDER BY score DESC\n",
"firstChangedLine": 21
}
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b905ae1fd1c0a06b",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Also, regarding example.py: it uses system.tables for table_exists — accessible in the playground. Good.
Note: example.py's show() function has a weird conditional print expression (ternary operator precedence bug):
print(
f"\n{len(rows)} rows | wall {wall_ms:.0f} ms | "
f"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB"
if str(read_bytes).isdigit()
else f"\n{len(rows)} rows | wall {wall_ms:.0f} ms"
)
This works (the ternary applies to the concatenated string), but int(read_bytes) could fail if it were a float string like "435400000" — that's fine. It output correctly. Still, I'll clean it up for readability.
Also, client.server_tz printed UTC — good.
Alright, re-test http_api.sh in full, and write a README describing pricing and notes. Additionally, verify that example.py's show() still works fine.
First, let me refactor that print into clearer code.
File edits
1 file
Modifiedclickhouse-demo/example.py+8−8View changes
/sandbox/repo/clickhouse-demo/example.py
Replacement excerpt
+ # The server reports what each query actually cost - the single most useful+ # thing for learning ClickHouse, because cost is driven by rows/bytes read.summary = result.summary or {}- read_rows = summary.get("read_rows", "n/a")- read_bytes = summary.get("read_bytes", "n/a")- print(- f"\n{len(rows)} rows | wall {wall_ms:.0f} ms | "- f"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB"- if str(read_bytes).isdigit()- else f"\n{len(rows)} rows | wall {wall_ms:.0f} ms"- )+ read_rows = str(summary.get("read_rows", ""))+ read_bytes = str(summary.get("read_bytes", ""))+ footer = f"\n{len(rows)} rows | wall {wall_ms:.0f} ms"+ if read_rows.isdigit() and read_bytes.isdigit():+ footer += f" | server scanned {int(read_rows):,} rows / {int(read_bytes) / 1e6:.1f} MB"+ print(footer)
Source: edit · chatcmpl-tool-abbfd1fc7b19a7bf
Recorded tool call
{
"type": "tool_call",
"seq": 93,
"ts": "2026-11-08T02:11:56.401Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/example.py",
"edits": [
{
"newText": " # The server reports what each query actually cost - the single most useful\n # thing for learning ClickHouse, because cost is driven by rows/bytes read.\n summary = result.summary or {}\n read_rows = str(summary.get(\"read_rows\", \"\"))\n read_bytes = str(summary.get(\"read_bytes\", \"\"))\n footer = f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n if read_rows.isdigit() and read_bytes.isdigit():\n footer += f\" | server scanned {int(read_rows):,} rows / {int(read_bytes) / 1e6:.1f} MB\"\n print(footer)",
"oldText": " summary = result.summary or {}\n read_rows = summary.get(\"read_rows\", \"n/a\")\n read_bytes = summary.get(\"read_bytes\", \"n/a\")\n print(\n f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms | \"\n f\"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB\"\n if str(read_bytes).isdigit()\n else f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n )"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-abbfd1fc7b19a7bf",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 94,
"ts": "2026-11-08T02:11:56.401Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 1 block(s) in /sandbox/repo/clickhouse-demo/example.py.",
"type": "text"
}
],
"details": {
"diff": " ...\n 118 print(\"-+-\".join(\"-\" * w for w in widths))\n 119 for row in rows:\n 120 print(fmt(row))\n 121 \n+122 # The server reports what each query actually cost - the single most useful\n+123 # thing for learning ClickHouse, because cost is driven by rows/bytes read.\n 122 summary = result.summary or {}\n-123 read_rows = summary.get(\"read_rows\", \"n/a\")\n-124 read_bytes = summary.get(\"read_bytes\", \"n/a\")\n-125 print(\n-126 f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms | \"\n-127 f\"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB\"\n-128 if str(read_bytes).isdigit()\n-129 else f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n-130 )\n+125 read_rows = str(summary.get(\"read_rows\", \"\"))\n+126 read_bytes = str(summary.get(\"read_bytes\", \"\"))\n+127 footer = f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n+128 if read_rows.isdigit() and read_bytes.isdigit():\n+129 footer += f\" | server scanned {int(read_rows):,} rows / {int(read_bytes) / 1e6:.1f} MB\"\n+130 print(footer)\n 131 \n 132 \n 133 def table_exists(client, database: str, table: str) -> bool:\n 134 res = client.query(\n ...",
"patch": "--- /sandbox/repo/clickhouse-demo/example.py\n+++ /sandbox/repo/clickhouse-demo/example.py\n@@ -118,17 +118,17 @@\n print(\"-+-\".join(\"-\" * w for w in widths))\n for row in rows:\n print(fmt(row))\n \n+ # The server reports what each query actually cost - the single most useful\n+ # thing for learning ClickHouse, because cost is driven by rows/bytes read.\n summary = result.summary or {}\n- read_rows = summary.get(\"read_rows\", \"n/a\")\n- read_bytes = summary.get(\"read_bytes\", \"n/a\")\n- print(\n- f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms | \"\n- f\"server scanned {read_rows} rows / {int(read_bytes) / 1e6:.1f} MB\"\n- if str(read_bytes).isdigit()\n- else f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n- )\n+ read_rows = str(summary.get(\"read_rows\", \"\"))\n+ read_bytes = str(summary.get(\"read_bytes\", \"\"))\n+ footer = f\"\\n{len(rows)} rows | wall {wall_ms:.0f} ms\"\n+ if read_rows.isdigit() and read_bytes.isdigit():\n+ footer += f\" | server scanned {int(read_rows):,} rows / {int(read_bytes) / 1e6:.1f} MB\"\n+ print(footer)\n \n \n def table_exists(client, database: str, table: str) -> bool:\n res = client.query(\n",
"firstChangedLine": 122
}
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-abbfd1fc7b19a7bf",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Next, let's write the README.md including the following:
- Contents of the demo
- How to run it (playground default / your own instance)
- Sample output snippets
- Pricing section (accurate, retrieved live)
- Manageability notes
- Known limitations (playground is read-only)
After that, run both scripts end-to-end to verify, then commit.
Pricing details to include (retrieved live from clickhouse.com/pricing, us-east-1, November 2025... wait, the current date is around November 8, 2026? The sandbox date shows November 8, and generate_rows outputted 2026-11-08. So "confirmed November 2026").
Rates:
- Basic: $0.2181/unit/hour, storage $25.30/TB/month, up to 1TB storage, 8–12 GiB total memory
- Scale: $0.2985/unit/hour
- Enterprise: $0.3903/unit/hour
- 1 compute unit = 2 vCPU + 8 GiB RAM, metered per minute, billed per CU-hour
- Idle scales to zero (compute), storage billed monthly
- Egress: to public internet $0.1152/GB, between regions $0.0312/GB
- ClickPipes is separate
- Trial: $300 in credits over 30 days
- OSS: permanently free (Apache 2.0)
Example of cost calculation: leave one Basic instance (minimum 0.25 units?) running 24/7 — I don't know the minimum unit count. The calculator mentioned "minimum compute per replica". Don't fabricate: give as an example "if you run 1 compute unit for 730 hours, compute is about $159/month + 100GB storage is $2.53/month". 0.2181 × 730 = $159.2. Storage: $25.30/TB → 100 GB ≈ $2.53. Okay, present it as an arithmetic example with a note that the Basic tier's minimum compute is less than 1 unit and scales to zero when idle.
Also mention that the demo itself cost $0 (the playground is free).
The README should also document what was verified and what wasn't (the write path is only verified via EXPLAIN and has not been executed because there are no writable credentials).
Alright, let's write it.
File edits
1 file
Writtenclickhouse-demo/README.mdView changes
/sandbox/repo/clickhouse-demo/README.md
Written content · previous contents were not recorded
# ClickHouse demo — hosted only, no local serverA minimal, runnable ClickHouse example that talks to a **hosted** ClickHouse overHTTPS. Nothing is installed or started locally except a Python package.```clickhouse-demo/├── example.py # official Python SDK (clickhouse-connect): analytics + write lifecycle├── http_api.sh # the same ideas over the raw HTTP interface with curl (zero deps)├── requirements.txt # clickhouse-connect└── README.md```## Run it```bashpip install -r requirements.txt # only clickhouse-connect (+ pandas if you want query_df)python3 example.py # default: public playground, no credentials./http_api.sh # same, with curl```Default target is ClickHouse's own hosted playground, `play.clickhouse.com`(user `play`, no password, free). It carries real datasets — ~37M Hacker Newsrows, ~43M cell towers, ClickBench, NOAA — so the analytics are genuinemulti-million-row aggregations that return in ~100 ms.### Against your own instance (ClickHouse Cloud, or any reachable server)```bashCLICKHOUSE_HOST=<your>.<region>.aws.clickhouse.cloud \CLICKHOUSE_USER=default \CLICKHOUSE_PASSWORD=<password> \python3 example.py```Self-hosted plain HTTP: `CLICKHOUSE_SECURE=false CLICKHOUSE_PORT=8123`.## What it shows**Read path** (`example.py` Part 1, runs anywhere the datasets exist):| # | Idea | Query shape ||---|------|-------------|| 1a | Columnar aggregation | `GROUP BY` + `avg`/`max` over 37M rows || 1b | Time bucketing + quantiles | `toYear(time)`, `countIf`, `quantile(0.95)` || 1c | `Enum8` columns | second dataset, 43M rows || 1d | Bind parameters | `{min_score:Int32}` — values sent separately from SQL || 1e | pandas integration | `client.query_df()` returns a typed DataFrame |Each result prints what the server reported it cost (`read_rows`, `read_bytes`),which is the thing to watch in ClickHouse: cost tracks bytes read, not rowsreturned. Example footer from a real run:```8 rows | wall 140 ms | server scanned 37,171,166 rows / 435.4 MB```**Write path** (`example.py` Part 2, needs DDL rights):`CREATE DATABASE` → `CREATE TABLE` (MergeTree, monthly partitions, `ORDER BY`primary key, `TTL`) → `client.insert()` of 5,000 generated rows (native columnarinsert, no SQL text) → aggregations → `ARRAY JOIN` → `Map` column access →`system.tables` compression stats → `DROP TABLE` cleanup.**HTTP API** (`http_api.sh`): output formats via `FORMAT` clause and the`default_format` URL arg, query-string bind parameters (`param_min_score=...`),auth headers (`X-ClickHouse-User` / `X-ClickHouse-Key`), `system` tables.## PricingVerified against `clickhouse.com/pricing` (N. Virginia / us-east-1) in Nov 2026.* **Open-source ClickHouse: free, forever** (Apache-2.0). Self-host and the onlycost is your hardware/ops. This demo's read path costs **$0** — the playgroundis free.* **ClickHouse Cloud (the hosted product): pure consumption pricing.** Compute andstorage are metered separately, and **compute scales to zero when idle**, so anidle dev instance bills storage only.* One **compute unit = 2 vCPUs + 8 GiB RAM**, metered per minute, billed percompute-unit-hour:| Plan | Compute | Storage | Notes ||------|---------|---------|-------|| Basic | **$0.2181**/unit/hr | $25.30 per 1 TB/mo | up to 1 TB storage, 8–12 GiB total memory, single replica || Scale | **$0.2985**/unit/hr | $25.30 per 1 TB/mo | 2+ AZs, unlimited storage, autoscaling, private networking || Enterprise | **$0.3903**/unit/hr | $25.30 per 1 TB/mo | CMEK, compliance, scheduled upgrades |* Storage is $25.30 per TB-month ≈ **$0.0247/GB-month** (~$2.53 for 100 GB),after ClickHouse's typical 5–10x compression.* Egress: public internet from **$0.1152/GB**, inter-region from **$0.0312/GB**.Ingest from ClickPipes is priced per connector type.* Trial: every new organisation gets **$300 of credits over 30 days**, no cardcharged until credits are gone. Change plan later without migration.* Rough maths: one always-on Basic compute unit ≈ 0.2181 × 730 h ≈ **$159/mo**compute + storage. A hobby project that idles most of the day costs a fewdollars; ClickHouse's own guidance is ~1:25 memory-to-storage for real-timeanalytics, so size memory first.## Verified vs. not* **Ran green:** the whole read path of `example.py` and `http_api.sh` against`play.clickhouse.com` (ClickHouse 26.10.3.7).* **Not executed:** Part 2 (write lifecycle). The playground is hard read-only —`CREATE DATABASE`, `CREATE TABLE` and even `CREATE TEMPORARY TABLE` all return`ACCESS_DENIED (code 497)` for user `play`, and no ClickHouse Cloud credentialswere available in this environment. Every Part-2 statement was thereforevalidated against the live server's parser with `EXPLAIN AST` / `EXPLAINSYNTAX` (all OK), and the insert payload was dry-run for column/type shape, butit has not been executed end-to-end. Point the script at a real instance (the$300 trial is enough) to run it.* Also blocked on the playground: `system.numbers`, `system.query_log`,`system.parts`, and changing settings such as `max_threads` (readonly mode).`http_api.sh` sticks to what the playground allows and notes the alternatives.## Notes for extending it* One client object = one connection pool; create it once and reuse it.* Use bind parameters (`{name:Type}` + `parameters=`), never f-strings, for values.* Batch inserts (thousands of rows per `insert()`); ClickHouse hates many tinyinserts — they create parts and force merges.* `ORDER BY` is the primary key/skip index: put your most selective filtercolumns first. `PARTITION BY` should be coarse (month), never high-cardinality.* `LowCardinality(String)` for enums-ish columns, `Array`/`Map`/`Nested` insteadof join tables — ClickHouse prefers denormalised wide tables.
Source: write · chatcmpl-tool-bc763f60b20cb1ed
Recorded tool call
{
"type": "tool_call",
"seq": 97,
"ts": "2026-11-08T02:12:14.564Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/clickhouse-demo/README.md",
"content": "# ClickHouse demo — hosted only, no local server\n\nA minimal, runnable ClickHouse example that talks to a **hosted** ClickHouse over\nHTTPS. Nothing is installed or started locally except a Python package.\n\n```\nclickhouse-demo/\n├── example.py # official Python SDK (clickhouse-connect): analytics + write lifecycle\n├── http_api.sh # the same ideas over the raw HTTP interface with curl (zero deps)\n├── requirements.txt # clickhouse-connect\n└── README.md\n```\n\n## Run it\n\n```bash\npip install -r requirements.txt # only clickhouse-connect (+ pandas if you want query_df)\n\npython3 example.py # default: public playground, no credentials\n./http_api.sh # same, with curl\n```\n\nDefault target is ClickHouse's own hosted playground, `play.clickhouse.com`\n(user `play`, no password, free). It carries real datasets — ~37M Hacker News\nrows, ~43M cell towers, ClickBench, NOAA — so the analytics are genuine\nmulti-million-row aggregations that return in ~100 ms.\n\n### Against your own instance (ClickHouse Cloud, or any reachable server)\n\n```bash\nCLICKHOUSE_HOST=<your>.<region>.aws.clickhouse.cloud \\\nCLICKHOUSE_USER=default \\\nCLICKHOUSE_PASSWORD=<password> \\\npython3 example.py\n```\n\nSelf-hosted plain HTTP: `CLICKHOUSE_SECURE=false CLICKHOUSE_PORT=8123`.\n\n## What it shows\n\n**Read path** (`example.py` Part 1, runs anywhere the datasets exist):\n\n| # | Idea | Query shape |\n|---|------|-------------|\n| 1a | Columnar aggregation | `GROUP BY` + `avg`/`max` over 37M rows |\n| 1b | Time bucketing + quantiles | `toYear(time)`, `countIf`, `quantile(0.95)` |\n| 1c | `Enum8` columns | second dataset, 43M rows |\n| 1d | Bind parameters | `{min_score:Int32}` — values sent separately from SQL |\n| 1e | pandas integration | `client.query_df()` returns a typed DataFrame |\n\nEach result prints what the server reported it cost (`read_rows`, `read_bytes`),\nwhich is the thing to watch in ClickHouse: cost tracks bytes read, not rows\nreturned. Example footer from a real run:\n\n```\n8 rows | wall 140 ms | server scanned 37,171,166 rows / 435.4 MB\n```\n\n**Write path** (`example.py` Part 2, needs DDL rights):\n`CREATE DATABASE` → `CREATE TABLE` (MergeTree, monthly partitions, `ORDER BY`\nprimary key, `TTL`) → `client.insert()` of 5,000 generated rows (native columnar\ninsert, no SQL text) → aggregations → `ARRAY JOIN` → `Map` column access →\n`system.tables` compression stats → `DROP TABLE` cleanup.\n\n**HTTP API** (`http_api.sh`): output formats via `FORMAT` clause and the\n`default_format` URL arg, query-string bind parameters (`param_min_score=...`),\nauth headers (`X-ClickHouse-User` / `X-ClickHouse-Key`), `system` tables.\n\n## Pricing\n\nVerified against `clickhouse.com/pricing` (N. Virginia / us-east-1) in Nov 2026.\n\n* **Open-source ClickHouse: free, forever** (Apache-2.0). Self-host and the only\n cost is your hardware/ops. This demo's read path costs **$0** — the playground\n is free.\n* **ClickHouse Cloud (the hosted product): pure consumption pricing.** Compute and\n storage are metered separately, and **compute scales to zero when idle**, so an\n idle dev instance bills storage only.\n * One **compute unit = 2 vCPUs + 8 GiB RAM**, metered per minute, billed per\n compute-unit-hour:\n | Plan | Compute | Storage | Notes |\n |------|---------|---------|-------|\n | Basic | **$0.2181**/unit/hr | $25.30 per 1 TB/mo | up to 1 TB storage, 8–12 GiB total memory, single replica |\n | Scale | **$0.2985**/unit/hr | $25.30 per 1 TB/mo | 2+ AZs, unlimited storage, autoscaling, private networking |\n | Enterprise | **$0.3903**/unit/hr | $25.30 per 1 TB/mo | CMEK, compliance, scheduled upgrades |\n * Storage is $25.30 per TB-month ≈ **$0.0247/GB-month** (~$2.53 for 100 GB),\n after ClickHouse's typical 5–10x compression.\n * Egress: public internet from **$0.1152/GB**, inter-region from **$0.0312/GB**.\n Ingest from ClickPipes is priced per connector type.\n * Trial: every new organisation gets **$300 of credits over 30 days**, no card\n charged until credits are gone. Change plan later without migration.\n * Rough maths: one always-on Basic compute unit ≈ 0.2181 × 730 h ≈ **$159/mo**\n compute + storage. A hobby project that idles most of the day costs a few\n dollars; ClickHouse's own guidance is ~1:25 memory-to-storage for real-time\n analytics, so size memory first.\n\n## Verified vs. not\n\n* **Ran green:** the whole read path of `example.py` and `http_api.sh` against\n `play.clickhouse.com` (ClickHouse 26.10.3.7).\n* **Not executed:** Part 2 (write lifecycle). The playground is hard read-only —\n `CREATE DATABASE`, `CREATE TABLE` and even `CREATE TEMPORARY TABLE` all return\n `ACCESS_DENIED (code 497)` for user `play`, and no ClickHouse Cloud credentials\n were available in this environment. Every Part-2 statement was therefore\n validated against the live server's parser with `EXPLAIN AST` / `EXPLAIN\n SYNTAX` (all OK), and the insert payload was dry-run for column/type shape, but\n it has not been executed end-to-end. Point the script at a real instance (the\n $300 trial is enough) to run it.\n* Also blocked on the playground: `system.numbers`, `system.query_log`,\n `system.parts`, and changing settings such as `max_threads` (readonly mode).\n `http_api.sh` sticks to what the playground allows and notes the alternatives.\n\n## Notes for extending it\n\n* One client object = one connection pool; create it once and reuse it.\n* Use bind parameters (`{name:Type}` + `parameters=`), never f-strings, for values.\n* Batch inserts (thousands of rows per `insert()`); ClickHouse hates many tiny\n inserts — they create parts and force merges.\n* `ORDER BY` is the primary key/skip index: put your most selective filter\n columns first. `PARTITION BY` should be coarse (month), never high-cardinality.\n* `LowCardinality(String)` for enums-ish columns, `Array`/`Map`/`Nested` instead\n of join tables — ClickHouse prefers denormalised wide tables.\n"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-bc763f60b20cb1ed",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 98,
"ts": "2026-11-08T02:12:14.564Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 5935 bytes to /sandbox/repo/clickhouse-demo/README.md",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-bc763f60b20cb1ed",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Next, I'll run a final end-to-end verification of both scripts. I'll check the exit codes and grep for error strings. I'll also run pyflakes/compile checks.
cd /sandbox/repo/clickhouse-demo && python3 -m py_compile example.py && echo "PY_COMPILE_OK"; timeout 240 python3 example.py > /tmp/py.out 2>/tmp/py.err; echo "PY_EXIT=$?"; grep -icE 'exception|error|traceback|denied' /tmp/py.out /tmp/py.err; echo "--- sections:"; grep -E '^(Part|host|server|mode)' /tmp/py.out | head -20; echo "--- stderr:"; cat /tmp/py.err
PY_COMPILE_OK
PY_COMPILE_OK PY_EXIT=0 /tmp/py.out:1 /tmp/py.err:0 --- sections: host : play.clickhouse.com:443 (https) mode : read-only (public playground) server : ClickHouse 26.10.3.7 (UTC) Part 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter) Part 1b: time bucketing + quantiles (quantile is approximate and very fast) Part 1c: Enum8 column, second dataset (~43M rows) Part 1d: parameterised query (bind variables, not string building) Part 1e: client.query_df() -> pandas DataFrame Part 2: write lifecycle --- stderr:
Recorded tool call
{
"type": "tool_call",
"seq": 101,
"ts": "2026-11-08T02:12:17.813Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && python3 -m py_compile example.py && echo \"PY_COMPILE_OK\"; timeout 240 python3 example.py > /tmp/py.out 2>/tmp/py.err; echo \"PY_EXIT=$?\"; grep -icE 'exception|error|traceback|denied' /tmp/py.out /tmp/py.err; echo \"--- sections:\"; grep -E '^(Part|host|server|mode)' /tmp/py.out | head -20; echo \"--- stderr:\"; cat /tmp/py.err"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9467ea05be181c45",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 103,
"ts": "2026-11-08T02:12:19.891Z",
"turn": 1,
"output": {
"content": [
{
"text": "PY_COMPILE_OK\nPY_EXIT=0\n/tmp/py.out:1\n/tmp/py.err:0\n--- sections:\nhost : play.clickhouse.com:443 (https)\nmode : read-only (public playground)\nserver : ClickHouse 26.10.3.7 (UTC)\nPart 1a: GROUP BY over ~37M rows (aggregation is ClickHouse's bread and butter)\nPart 1b: time bucketing + quantiles (quantile is approximate and very fast)\nPart 1c: Enum8 column, second dataset (~43M rows)\nPart 1d: parameterised query (bind variables, not string building)\nPart 1e: client.query_df() -> pandas DataFrame\nPart 2: write lifecycle\n--- stderr:\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-9467ea05be181c45",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo/clickhouse-demo && bash -n http_api.sh && echo "BASH_SYNTAX_OK"; timeout 240 ./http_api.sh > /tmp/sh.out 2>/tmp/sh.err; echo "SH_EXIT=$?"; grep -cE 'Code: [0-9]+' /tmp/sh.out; echo "--- error lines (only item 7 expected):"; grep -nE 'Code: [0-9]+' /tmp/sh.out
BASH_SYNTAX_OK
BASH_SYNTAX_OK SH_EXIT=0 1 --- error lines (only item 7 expected): 105:Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default.nope_from_demo. (ACCESS_DENIED) (version 26.10.3.7 (official build))
Recorded tool call
{
"type": "tool_call",
"seq": 102,
"ts": "2026-11-08T02:12:17.813Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo/clickhouse-demo && bash -n http_api.sh && echo \"BASH_SYNTAX_OK\"; timeout 240 ./http_api.sh > /tmp/sh.out 2>/tmp/sh.err; echo \"SH_EXIT=$?\"; grep -cE 'Code: [0-9]+' /tmp/sh.out; echo \"--- error lines (only item 7 expected):\"; grep -nE 'Code: [0-9]+' /tmp/sh.out"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-8f82d619ac5c53ec",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 104,
"ts": "2026-11-08T02:12:19.891Z",
"turn": 1,
"output": {
"content": [
{
"text": "BASH_SYNTAX_OK\nSH_EXIT=0\n1\n--- error lines (only item 7 expected):\n105:Code: 497. DB::Exception: play: Not enough privileges. To execute this query, it's necessary to have the grant CREATE TABLE ON default.nope_from_demo. (ACCESS_DENIED) (version 26.10.3.7 (official build))\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-8f82d619ac5c53ec",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Both scripts completed successfully (exit 0). The only "error" matches are as follows:
- py.out: 1 hit — this is the "ACCESS_DENIED, code 497" mentioned in the explanatory text of Part 2, which is intentional.
- sh.out: 1 hit on line 105 — this is an intentionally demonstrated write rejection.
I'll re-check the entire shell output to confirm that items 2, 5, and 6 are producing the correct results.
✓read /tmp/sh.out✓└{"content":[{"text":"\t\t\"elapsed\": 0.001228642,\n\t\t\"rows_read\": 1,\n\t\t\"bytes_read\": 1\n\t}\n}\n\n== 3. aggregation over a hosted dataset (~37M rows)\…ctrl+o
{"content":[{"text":"\t\t\"elapsed\": 0.001228642,\n\t\t\"rows_read\": 1,\n\t\t\"bytes_read\": 1\n\t}\n}\n\n== 3. aggregation over a hosted dataset (~37M rows)\n\nSELECT toYear(time) AS year, count() AS items, round(avg(score), 1) AS avg_score\nFROM default.hackernews\nWHERE time >= toDateTime('2019-01-01')\nGROUP BY year\nORDER BY year DESC\nLIMIT 6\nFORMAT PrettyCompactMonoBlock\n\n ┌─year─┬───items─┬─avg_score─┐\n1. │ 2023 │ 2969111 │ 1.5 │\n2. │ 2022 │ 4448191 │ 1.5 │\n3. │ 2021 │ 4155060 │ 1.5 │\n4. │ 2020 │ 3673138 │ 1.7 │\n5. │ 2019 │ 3127712 │ 1.8 │\n └──────┴─────────┴───────────┘\n\n== 4. bind parameters on the query string (never interpolate user input into SQL)\n\nSELECT id, score, left(title, 50) AS title\nFROM default.hackernews\nWHERE type = 'story' AND score >= {min_score:Int32}\nORDER BY score DESC\nLIMIT 3\nFORMAT TSVWithNames\n\nid\tscore\ttitle\n16582136\t6015\tStephen Hawking has died\n11116274\t5771\tA Message to Our Customers\n3078128\t4338\tSteve Jobs has passed away.\n\n== 5. output format can also be set as an HTTP query-string arg (default_format=CSV)\n\n16582136,6015,\"Stephen Hawking has died\"\n11116274,5771,\"A Message to Our Customers\"\n3078128,4338,\"Steve Jobs has passed away.\"\n\n== 6. system tables = free observability (here: what is on this server)\n\nSELECT database, count() AS tables, formatReadableSize(sum(total_bytes)) AS on_disk,\n sum(total_rows) AS rows\nFROM system.tables\nWHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')\nGROUP BY database\nORDER BY sum(total_bytes) DESC\nLIMIT 5\nFORMAT PrettyCompactMonoBlock\n\n ┌─database───────┬─tables─┬─on_disk────┬────────rows─┐\n1. │ default │ 69 │ 911.59 GiB │ 19393357697 │ -- 19.39 billion\n2. │ blogs │ 9 │ 141.35 GiB │ 17785193865 │ -- 17.79 billion\n3. │ mgbench │ 3 │ 2.66 GiB │ 206729016 │ -- 206.73 million\n4. │ git_clickhouse │ 3 │ 260.33 MiB │ 7863993 │ -- 7.86 million\n\n[10 more lines in file. Use offset=100 to continue.]","type":"text"}]}›Thinking…
Everything is working. Next, commit the work and tidy up the temp files (pricing.html in /tmp is outside the repo so it's fine). Check git status and commit.
Also, pycache from py_compile — .gitignore covers pycache/. Let me check git status.
cd /sandbox/repo && git status --short && git add clickhouse-demo && git -c user.name=pi -c user.email=pi@local commit -q -m "Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes" && git log --oneline -2 && echo "--- files:" && ls -la clickhouse-demo
?? clickhouse-demo/
?? clickhouse-demo/ a1b36bd Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes 9ebf5fb Initial workspace --- files: total 44 drwx------ 3 root root 4096 Nov 8 02:12 . drwxr-xr-x 4 root root 4096 Nov 8 02:10 .. -rw------- 1 root root 6000 Nov 8 02:12 README.md drwx------ 2 root root 4096 Nov 8 02:12 __pycache__ -rwx------ 1 root root 15143 Nov 8 02:11 example.py -rwx------ 1 root root 3500 Nov 8 02:11 http_api.sh -rw------- 1 root root 26 Nov 8 02:11 requirements.txt
Recorded tool call
{
"type": "tool_call",
"seq": 111,
"ts": "2026-11-08T02:12:24.853Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && git status --short && git add clickhouse-demo && git -c user.name=pi -c user.email=pi@local commit -q -m \"Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes\" && git log --oneline -2 && echo \"--- files:\" && ls -la clickhouse-demo"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-bc8ba854362a0644",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 112,
"ts": "2026-11-08T02:12:24.853Z",
"turn": 1,
"output": {
"content": [
{
"text": "?? clickhouse-demo/\na1b36bd Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes\n9ebf5fb Initial workspace\n--- files:\ntotal 44\ndrwx------ 3 root root 4096 Nov 8 02:12 .\ndrwxr-xr-x 4 root root 4096 Nov 8 02:10 ..\n-rw------- 1 root root 6000 Nov 8 02:12 README.md\ndrwx------ 2 root root 4096 Nov 8 02:12 __pycache__\n-rwx------ 1 root root 15143 Nov 8 02:11 example.py\n-rwx------ 1 root root 3500 Nov 8 02:11 http_api.sh\n-rw------- 1 root root 26 Nov 8 02:11 requirements.txt\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-bc8ba854362a0644",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Committed (pycache is ignored by .gitignore — confirmed: the fact that only clickhouse-demo/ was added means pycache is excluded. Since git status showed everything as "?? clickhouse-demo/", let me verify that pycache isn't committed).
Quick check: git show --stat. Then done — write the final answer covering the following:
- What was built and how to run it
- How the pricing structure works
- Whether it's easy for me to manage (assessment of the developer workflow)
- Blockers: playground is read-only → no credentials, so the write path wasn't executed.
cd /sandbox/repo && git show --stat --oneline HEAD | head -10
a1b36bd Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes
a1b36bd Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes clickhouse-demo/README.md | 122 +++++++++++ clickhouse-demo/example.py | 451 +++++++++++++++++++++++++++++++++++++++ clickhouse-demo/http_api.sh | 93 ++++++++ clickhouse-demo/requirements.txt | 1 + 4 files changed, 667 insertions(+)
Recorded tool call
{
"type": "tool_call",
"seq": 115,
"ts": "2026-11-08T02:12:27.683Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && git show --stat --oneline HEAD | head -10"
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b214750d75b9aecd",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 116,
"ts": "2026-11-08T02:12:27.683Z",
"turn": 1,
"output": {
"content": [
{
"text": "a1b36bd Add hosted-only ClickHouse demo: SDK example, raw HTTP API script, pricing notes\n clickhouse-demo/README.md | 122 +++++++++++\n clickhouse-demo/example.py | 451 +++++++++++++++++++++++++++++++++++++++\n clickhouse-demo/http_api.sh | 93 ++++++++\n clickhouse-demo/requirements.txt | 1 +\n 4 files changed, 667 insertions(+)\n",
"type": "text"
}
]
},
"run_id": "cmw76z9wv05ih0iu2ppf8kcmo",
"tool_id": "chatcmpl-tool-b214750d75b9aecd",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Wait a sec — example.py is 451 lines? What I wrote should have been about 430 lines... whatever, fine.
Now, on to the final answer. Keep it concise but cover the following:
- What was built + how to run it + evidence it actually works
- How the pricing structure works
- Manageability judgment (can it be easily managed)
- Blockers (being honest): playground is read-only → Part 2 was not executed; there is a developer workflow so I didn't stop.
Built and verified against a real hosted ClickHouse — no local stack, nothing long-running.
What's there — /sandbox/repo/clickhouse-demo/ (committed, a1b36bd)
| File | What it does |
|---|---|
example.py | Official Python SDK (clickhouse-connect) — analytics + full write lifecycle |
http_api.sh | Same ideas over ClickHouse's raw HTTP interface with curl (zero deps) |
requirements.txt | clickhouse-connect |
README.md | Run instructions, pricing, verified-vs-not notes |
pip install -r requirements.txt
python3 example.py # default target: play.clickhouse.com (free, no credentials)
./http_api.sh
Ran green, exit 0: read path against the hosted playground (ClickHouse 26.10.3.7) — real multi-million-row aggregations: GROUP BY over 37M Hacker News rows (140 ms, 37,171,166 rows scanned), quantile(0.95) time bucketing, Enum8 over 43M cell towers, bind parameters ({min_score:Int32}), and client.query_df() → typed pandas DataFrame. http_api.sh demos FORMAT/default_format, query-string bind params, auth headers, system tables.
Pricing (pulled live from clickhouse.com/pricing, us-east-1)
- OSS ClickHouse: free forever (Apache-2.0). This demo's read path cost $0.
- ClickHouse Cloud: pure consumption. Compute and storage metered separately; compute scales to zero when idle, so an idle instance bills storage only.
- 1 compute unit = 2 vCPU + 8 GiB RAM, metered per minute, billed per unit-hour: Basic $0.2181, Scale $0.2985, Enterprise $0.3903. Storage $25.30/TB-month (~$0.0247/GB, ~$2.53 per 100 GB) on all tiers; Basic caps at 1 TB / 8–12 GiB memory.
- Egress from $0.1152/GB (internet), $0.0312/GB (inter-region); ClickPipes priced per connector.
- Trial: $300 credits / 30 days, card not charged until credits run out.
- Ballpark: one always-on Basic unit ≈ 0.2181 × 730 h ≈ $159/mo + storage; a demo that idles costs a few dollars.
Is it easy for me to manage? Yes
Fully actionable developer workflow: plain SQL over HTTPS, one pip package, no daemons, no ports, no Docker. Errors are precise (code + required grant), system.* tables give free observability, and each query reports read_rows/read_bytes so I can reason about cost directly. Two sharp edges worth knowing: DDL perms are strict (playground denies everything, code 497), and readonly connections reject setting changes like max_threads.
Where I got blocked (one thing, worked around)
play.clickhouse.com is hard read-only — CREATE DATABASE, CREATE TABLE, even CREATE TEMPORARY TABLE return ACCESS_DENIED (497), and no ClickHouse Cloud credentials exist in this environment. So Part 2 (CREATE → INSERT 5,000 rows → aggregate → ARRAY JOIN → Map access → DROP) is written but never executed. I validated every statement against the live server's parser via EXPLAIN AST/EXPLAIN SYNTAX (all OK) and dry-ran the insert payload for column/type shape. To actually run it, give me a host/user/password (the $300 trial suffices):
CLICKHOUSE_HOST=<host> CLICKHOUSE_USER=default CLICKHOUSE_PASSWORD=<pw> python3 example.py
http_api.sh also avoids what the playground blocks (system.numbers, system.query_log, system.parts, settings changes) and notes the equivalents for a real instance.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents