clickhawk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. clickhawk-0.1.0/.claude/settings.local.json +28 -0
  2. clickhawk-0.1.0/.env.example +5 -0
  3. clickhawk-0.1.0/.gitattributes +2 -0
  4. clickhawk-0.1.0/.gitignore +50 -0
  5. clickhawk-0.1.0/CHANGELOG.md +39 -0
  6. clickhawk-0.1.0/CHANGELOG_CN.md +41 -0
  7. clickhawk-0.1.0/LESSONS_LEARNED.md +245 -0
  8. clickhawk-0.1.0/LESSONS_LEARNED_CN.md +245 -0
  9. clickhawk-0.1.0/PKG-INFO +372 -0
  10. clickhawk-0.1.0/README.md +347 -0
  11. clickhawk-0.1.0/README_CN.md +347 -0
  12. clickhawk-0.1.0/STRUCTURE.md +155 -0
  13. clickhawk-0.1.0/STRUCTURE_CN.md +149 -0
  14. clickhawk-0.1.0/TUTORIAL.md +421 -0
  15. clickhawk-0.1.0/TUTORIAL_CN.md +418 -0
  16. clickhawk-0.1.0/clickhawk/__init__.py +1 -0
  17. clickhawk-0.1.0/clickhawk/commands/__init__.py +0 -0
  18. clickhawk-0.1.0/clickhawk/commands/migrate.py +17 -0
  19. clickhawk-0.1.0/clickhawk/commands/monitor.py +53 -0
  20. clickhawk-0.1.0/clickhawk/commands/profile.py +61 -0
  21. clickhawk-0.1.0/clickhawk/commands/query.py +24 -0
  22. clickhawk-0.1.0/clickhawk/commands/schema.py +74 -0
  23. clickhawk-0.1.0/clickhawk/commands/slowlog.py +52 -0
  24. clickhawk-0.1.0/clickhawk/core/__init__.py +0 -0
  25. clickhawk-0.1.0/clickhawk/core/client.py +20 -0
  26. clickhawk-0.1.0/clickhawk/core/config.py +12 -0
  27. clickhawk-0.1.0/clickhawk/formatters/__init__.py +0 -0
  28. clickhawk-0.1.0/clickhawk/formatters/table.py +26 -0
  29. clickhawk-0.1.0/clickhawk/formatters/tree.py +1 -0
  30. clickhawk-0.1.0/clickhawk/main.py +40 -0
  31. clickhawk-0.1.0/clickhawk/utils/__init__.py +0 -0
  32. clickhawk-0.1.0/clickhawk/utils/sql.py +1 -0
  33. clickhawk-0.1.0/clickhawk-plan.docx +0 -0
  34. clickhawk-0.1.0/examples/BASIC_QUERY.md +111 -0
  35. clickhawk-0.1.0/examples/BASIC_QUERY_CN.md +111 -0
  36. clickhawk-0.1.0/examples/MONITORING.md +106 -0
  37. clickhawk-0.1.0/examples/MONITORING_CN.md +125 -0
  38. clickhawk-0.1.0/examples/PROFILING.md +120 -0
  39. clickhawk-0.1.0/examples/PROFILING_CN.md +120 -0
  40. clickhawk-0.1.0/examples/SCHEMA_EXPLORATION.md +109 -0
  41. clickhawk-0.1.0/examples/SCHEMA_EXPLORATION_CN.md +111 -0
  42. clickhawk-0.1.0/pyproject.toml +53 -0
  43. clickhawk-0.1.0/tests/__init__.py +0 -0
  44. clickhawk-0.1.0/tests/conftest.py +20 -0
  45. clickhawk-0.1.0/tests/integration/__init__.py +0 -0
  46. clickhawk-0.1.0/tests/integration/conftest.py +30 -0
  47. clickhawk-0.1.0/tests/integration/test_health.py +34 -0
  48. clickhawk-0.1.0/tests/integration/test_profile.py +42 -0
  49. clickhawk-0.1.0/tests/integration/test_query.py +60 -0
  50. clickhawk-0.1.0/tests/integration/test_schema.py +43 -0
  51. clickhawk-0.1.0/tests/integration/test_slowlog.py +36 -0
  52. clickhawk-0.1.0/tests/unit/__init__.py +0 -0
  53. clickhawk-0.1.0/tests/unit/test_config.py +45 -0
  54. clickhawk-0.1.0/tests/unit/test_formatters.py +118 -0
  55. clickhawk-0.1.0/tests/unit/test_query_logic.py +79 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(which clickhouse:*)",
5
+ "Bash(pip show:*)",
6
+ "Bash(pip3 show:*)",
7
+ "Bash(which ch:*)",
8
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/pip show:*)",
9
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/ch query:*)",
10
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/pip install:*)",
11
+ "Bash(CH=/Users/zhenningli/.pyenv/versions/3.14.2/bin/ch\n\necho \"=== ch health ===\"\n$CH health\n\necho \"\"\necho \"=== ch query table ===\"\n$CH query 'SELECT event_type, count\\(\\) AS cnt FROM demo.events GROUP BY event_type ORDER BY cnt DESC'\n\necho \"\"\necho \"=== ch query --format json ===\"\n$CH query 'SELECT event_type, count\\(\\) AS cnt FROM demo.events GROUP BY event_type' --format json\n\necho \"\"\necho \"=== ch query --format csv ===\"\n$CH query 'SELECT user_id, count\\(\\) AS cnt FROM demo.events GROUP BY user_id ORDER BY cnt DESC LIMIT 5' --format csv\n\necho \"\"\necho \"=== ch query --limit ===\"\n$CH query 'SELECT * FROM demo.events' --limit 3\n\necho \"\"\necho \"=== ch schema tables ===\"\n$CH schema tables --database demo\n\necho \"\"\necho \"=== ch schema show ===\"\n$CH schema show events --database demo\n\necho \"\"\necho \"=== ch slowlog ===\"\n$CH slowlog --threshold 1 --hours 1 --top 5\n\necho \"\"\necho \"=== ch profile ===\"\n$CH profile 'SELECT uniq\\(user_id\\) FROM demo.events WHERE date = today\\(\\)')",
12
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/python -m pytest tests/unit/ -v 2>&1)",
13
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/python -c \"import clickhawk.formatters.table as m; import inspect; print\\(inspect.signature\\(m.print_result\\)\\)\")",
14
+ "Bash(/Users/zhenningli/.pyenv/versions/3.14.2/bin/python -m pytest tests/integration/ -v -m integration 2>&1)",
15
+ "Bash(for f:*)",
16
+ "Bash(do echo:*)",
17
+ "Read(//Users/zhenningli/Documents/GitHub/clickhawk/**)",
18
+ "Bash(done)",
19
+ "Bash(git mv:*)",
20
+ "Bash(cat:*)",
21
+ "Bash(git rm:*)",
22
+ "Bash(echo:*)",
23
+ "Bash(which python3:*)",
24
+ "Bash(~/.pyenv/shims/python -m pytest tests/unit/ -q 2>&1 | tail -5)",
25
+ "Bash(~/.pyenv/shims/python -m pytest tests/integration/ -m integration -q 2>&1 | tail -5)"
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ CH_HOST=localhost
2
+ CH_PORT=8123
3
+ CH_USER=default
4
+ CH_PASSWORD=
5
+ CH_DATABASE=default
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,50 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ wheels/
12
+ sdist/
13
+ .eggs/
14
+
15
+ # 虚拟环境
16
+ .venv/
17
+ venv/
18
+ env/
19
+ ENV/
20
+
21
+ # 环境配置(含敏感信息)
22
+ .env
23
+ .env.local
24
+ .env.*.local
25
+
26
+ # 类型检查 / linter / 测试缓存
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+ .pytest_cache/
30
+ .coverage
31
+ coverage.xml
32
+ htmlcov/
33
+
34
+ # 编辑器
35
+ .DS_Store
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+ *.swo
40
+
41
+ # ClickHouse 本地数据(tutorial.md 中创建的本地测试目录)
42
+ clickhouse-data/
43
+ clickhouse-config.xml
44
+ /tmp/clickhouse.log
45
+
46
+ # 日志
47
+ *.log
48
+
49
+ # pyproject 构建产物
50
+ *.whl
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ This project follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format, and versioning follows [Semantic Versioning](https://semver.org/).
4
+
5
+ ---
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Planned
10
+ - `ch explain` — colorized tree-style EXPLAIN PLAN output
11
+ - `ch schema diff` — compare schema differences between two ClickHouse environments
12
+ - `ch migrate run/status` — file-based schema migration management
13
+
14
+ ---
15
+
16
+ ## [0.1.0] — 2026-03-16
17
+
18
+ ### Added
19
+ - **`ch health`** — cluster health check displaying version, uptime, database count, and table count
20
+ - **`ch query <sql>`** — execute SQL queries with support for `table` (default) / `json` / `csv` output formats and `--limit` row truncation
21
+ - **`ch profile <sql>`** — query performance analysis extracting real execution statistics from `system.query_log` (wall time, DB time, rows/bytes read, memory, parts/ranges hit count)
22
+ - **`ch slowlog`** — slow query history ranking with customizable `--top`, `--threshold` (milliseconds), and `--hours` parameters
23
+ - **`ch schema show <table>`** — display table schema (column name / type / default / comment) with `--database` filter support
24
+ - **`ch schema tables`** — list all user tables showing database, engine, disk size, and row count with `--database` filter support
25
+ - **`ch monitor`** — real-time live dashboard polling `system.processes`, with color-coded long-running queries (>5s yellow, >30s red) and customizable `--interval` refresh rate
26
+ - **`ch migrate run/status`** — placeholder commands, to be fully implemented in v0.2
27
+
28
+ ### Technical Foundation
29
+ - CLI framework built on **Typer** with `ch` as the command entry point
30
+ - **Rich** for colorized table rendering and Live real-time refresh
31
+ - **clickhouse-connect** for ClickHouse connectivity (HTTP protocol)
32
+ - **Pydantic Settings v2** for configuration management with `.env` file and environment variable support
33
+ - Singleton pattern for the connection client to avoid repeated handshakes
34
+ - Python 3.13+ support
35
+
36
+ ---
37
+
38
+ [Unreleased]: https://github.com/your-username/clickhawk/compare/v0.1.0...HEAD
39
+ [0.1.0]: https://github.com/your-username/clickhawk/releases/tag/v0.1.0
@@ -0,0 +1,41 @@
1
+ > English version: [CHANGELOG.md](CHANGELOG.md)
2
+
3
+ # 更新日志
4
+
5
+ 本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/) 格式,版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
6
+
7
+ ---
8
+
9
+ ## [未发布]
10
+
11
+ ### 计划中
12
+ - `ch explain` — 彩色树形 EXPLAIN PLAN 输出
13
+ - `ch schema diff` — 对比两个 ClickHouse 环境的 schema 差异
14
+ - `ch migrate run/status` — 基于文件的 schema 迁移管理
15
+
16
+ ---
17
+
18
+ ## [0.1.0] — 2026-03-16
19
+
20
+ ### 新增
21
+ - **`ch health`** — 集群健康检查,显示版本、uptime、数据库数量和表数量
22
+ - **`ch query <sql>`** — 执行 SQL 查询,支持 `table`(默认)/`json`/`csv` 三种输出格式,支持 `--limit` 截断行数
23
+ - **`ch profile <sql>`** — 查询性能分析,通过 `system.query_log` 提取真实执行统计(wall time、DB 耗时、读取行数/字节数、内存、parts/ranges 命中数)
24
+ - **`ch slowlog`** — 慢查询历史排行,支持 `--top`、`--threshold`(毫秒)、`--hours` 参数自定义
25
+ - **`ch schema show <table>`** — 展示表结构(列名/类型/默认值/注释),支持 `--database` 过滤
26
+ - **`ch schema tables`** — 列出所有用户表,展示数据库、引擎、磁盘大小和行数,支持 `--database` 过滤
27
+ - **`ch monitor`** — 实时 Live Dashboard,轮询 `system.processes`,超时查询以颜色区分(>5s 黄色,>30s 红色),支持 `--interval` 自定义刷新频率
28
+ - **`ch migrate run/status`** — 占位命令,v0.2 正式实现
29
+
30
+ ### 技术基础
31
+ - 基于 **Typer** 构建 CLI 框架,命令入口为 `ch`
32
+ - 使用 **Rich** 渲染彩色表格和 Live 实时刷新
33
+ - 通过 **clickhouse-connect** 连接 ClickHouse(HTTP 协议)
34
+ - 使用 **Pydantic Settings v2** 管理配置,支持 `.env` 文件和环境变量
35
+ - 连接客户端采用单例模式,避免重复握手
36
+ - 支持 Python 3.13+
37
+
38
+ ---
39
+
40
+ [未发布]: https://github.com/your-username/clickhawk/compare/v0.1.0...HEAD
41
+ [0.1.0]: https://github.com/your-username/clickhawk/releases/tag/v0.1.0
@@ -0,0 +1,245 @@
1
+ > 中文版: [LESSONS_LEARNED_CN.md](LESSONS_LEARNED_CN.md)
2
+
3
+ # Lessons Learned
4
+
5
+ Pitfalls encountered and lessons learned during ClickHawk development, for reference by future developers and contributors.
6
+
7
+ ---
8
+
9
+ ## 1. macOS Gatekeeper Blocking ClickHouse Binaries Installed via Homebrew Tap
10
+
11
+ **Problem:**
12
+ After installing via `brew tap clickhouse/clickhouse && brew install clickhouse`, executing `clickhouse` in the terminal produces an error stating "cannot be opened because the developer cannot be verified", or the binary is silently blocked by macOS.
13
+
14
+ **Root Cause:**
15
+ The official ClickHouse tap installs a pre-compiled binary. macOS Gatekeeper adds a quarantine attribute to binaries that have not been notarized by Apple, preventing them from running.
16
+
17
+ **Solution:**
18
+ ```bash
19
+ xattr -d com.apple.quarantine $(which clickhouse)
20
+ ```
21
+
22
+ Takes effect immediately without requiring a restart.
23
+
24
+ **Appendix: `brew services start clickhouse` Error**
25
+ The `clickhouse/clickhouse` tap installs a single-file binary without registering a Homebrew service (no launchd plist), so `brew services` cannot manage it.
26
+ The correct way to start it is to run it directly (a complete config.xml must be provided — see next item):
27
+ ```bash
28
+ clickhouse server --config-file=$HOME/clickhouse-config.xml &
29
+ ```
30
+
31
+ **`ch slowlog` / `ch profile` Reporting `Unknown table 'system.query_log'`**
32
+ When `config.xml` lacks a `<query_log>` configuration block, ClickHouse does not create the `system.query_log` table (lazy initialization), causing slowlog and profile commands to fail. Solution: add a `<query_log>` node to `config.xml` and set `<log_queries>1</log_queries>` in the default profile.
33
+
34
+ **A minimal working `config.xml` must include profiles/users/quotas**, otherwise the error `Settings profile 'default' not found` (exit 180) will occur. Path and port configuration alone is insufficient — ClickHouse looks for the `default` profile at startup:
35
+ ```xml
36
+ <clickhouse>
37
+ <path>...</path>
38
+ <tmp_path>...</tmp_path>
39
+ <tcp_port>9000</tcp_port>
40
+ <http_port>8123</http_port>
41
+ <listen_host>127.0.0.1</listen_host>
42
+ <profiles>
43
+ <default><max_memory_usage>10000000000</max_memory_usage></default>
44
+ </profiles>
45
+ <users>
46
+ <default>
47
+ <password></password>
48
+ <networks><ip>::/0</ip></networks>
49
+ <profile>default</profile>
50
+ <quota>default</quota>
51
+ </default>
52
+ </users>
53
+ <quotas>
54
+ <default>
55
+ <interval>
56
+ <duration>3600</duration>
57
+ <queries>0</queries><errors>0</errors>
58
+ <result_rows>0</result_rows><read_rows>0</read_rows>
59
+ <execution_time>0</execution_time>
60
+ </interval>
61
+ </default>
62
+ </quotas>
63
+ </clickhouse>
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 2. Typer 0.12+: Options Cannot Follow Positional Arguments in Sub-Apps
69
+
70
+ **Problem:**
71
+ `ch query 'SELECT ...' --format json` reports `Missing argument 'SQL'`, while `ch query --format json 'SELECT ...'` works correctly.
72
+
73
+ **Root Cause:**
74
+ In Typer 0.12+ when using `add_typer()` for nested sub-apps, the sub-app's callback does not allow options (e.g., `--format`) to appear after positional arguments (e.g., SQL) by default — which is counter-intuitive.
75
+
76
+ **Solution:**
77
+ Add `context_settings={"allow_interspersed_args": True}` to each sub-app that combines positional arguments with options:
78
+ ```python
79
+ app = typer.Typer(help="...", context_settings={"allow_interspersed_args": True})
80
+ ```
81
+ Affected commands: `query`, `profile`, `slowlog`, `monitor`.
82
+
83
+ ---
84
+
85
+ ## 3. ClickHouse 26.x Crashes on macOS ARM64 Without a Config File
86
+
87
+ **Problem:**
88
+ Running `./clickhouse server &` on macOS Apple Silicon causes the server to crash immediately with exit code 91 and the error:
89
+
90
+ ```
91
+ DB::Exception: Poco::Exception. Code: 1000, e.code() = 0, Null pointer
92
+ DB::Context::setClustersConfig ... CANNOT_LOAD_CONFIG
93
+ ```
94
+
95
+ **Root Cause:**
96
+ ClickHouse 26.3 (and some 26.x versions) triggers a null pointer crash in `setClustersConfig` when starting in embedded config mode (without a `config.xml`) on macOS ARM64. This is a known ClickHouse bug, not a system or permissions issue.
97
+
98
+ **Solutions:**
99
+
100
+ Option 1 (recommended): Use Docker to avoid configuration issues entirely:
101
+ ```bash
102
+ docker run -d --name clickhouse-local \
103
+ -p 8123:8123 -p 9000:9000 \
104
+ --ulimit nofile=262144:262144 \
105
+ clickhouse/clickhouse-server
106
+ ```
107
+
108
+ Option 2: Provide a minimal `config.xml` to work around the bug:
109
+ ```bash
110
+ mkdir -p ~/clickhouse-data/{data,logs,tmp}
111
+ cat > config.xml << 'EOF'
112
+ <clickhouse>
113
+ <path>clickhouse-data/data/</path>
114
+ <tmp_path>clickhouse-data/tmp/</tmp_path>
115
+ <tcp_port>9000</tcp_port>
116
+ <http_port>8123</http_port>
117
+ <listen_host>127.0.0.1</listen_host>
118
+ </clickhouse>
119
+ EOF
120
+ ./clickhouse server --config-file=config.xml &
121
+ ```
122
+
123
+ **Takeaway:**
124
+ The official single-file binary is unstable on macOS (especially newer versions). Docker is the preferred approach for local development. On Linux, single-file mode generally works without this issue.
125
+
126
+ ---
127
+
128
+ ## 4. Delayed Writes to `system.query_log`
129
+
130
+ **Problem:**
131
+ In the `ch profile` command, querying `system.query_log` immediately after executing a query returns empty results, causing performance statistics to not display.
132
+
133
+ **Root Cause:**
134
+ ClickHouse's `query_log` is not written synchronously. There is an asynchronous flush delay after a query completes (typically 100–500ms, depending on server load and configuration).
135
+
136
+ **Solution:**
137
+ After executing the target query, call `time.sleep(0.3)` to wait 300ms before querying `system.query_log`. This is a trade-off: too short a wait risks missing data, while too long a wait degrades user experience.
138
+
139
+ ```python
140
+ client.query(sql, settings={"log_queries": 1, "query_id": query_id})
141
+ time.sleep(0.3) # wait for async query_log flush
142
+ stats = client.query(f"SELECT ... FROM system.query_log WHERE query_id = '{query_id}'")
143
+ ```
144
+
145
+ **Takeaway:**
146
+ ClickHouse system tables (`system.query_log`, `system.part_log`, etc.) are typically written asynchronously. Do not assume they are immediately consistent. If you encounter empty results, delayed writes should be the first thing to investigate.
147
+
148
+ ---
149
+
150
+ ## 5. Accessing `ProfileEvents` Fields
151
+
152
+ **Problem:**
153
+ The `ProfileEvents` field in `system.query_log` is of type `Map(String, UInt64)`. Accessing a specific key requires the `ProfileEvents['KeyName']` syntax, not dot notation.
154
+
155
+ **Correct usage:**
156
+ ```sql
157
+ SELECT
158
+ ProfileEvents['SelectedParts'] AS parts_selected,
159
+ ProfileEvents['SelectedRanges'] AS ranges_selected
160
+ FROM system.query_log
161
+ WHERE query_id = '...'
162
+ ```
163
+
164
+ **Commonly used ProfileEvents keys:**
165
+ - `SelectedParts` — number of parts hit by the query (fewer is better, indicating effective partition pruning)
166
+ - `SelectedRanges` — number of ranges hit
167
+ - `SelectedMarks` — number of marks hit (related to index granularity)
168
+ - `RealTimeMicroseconds` — actual elapsed time in microseconds
169
+ - `UserTimeMicroseconds` — user-space CPU time
170
+
171
+ ---
172
+
173
+ ## 6. Conflict Between Typer Sub-Commands and `callback`
174
+
175
+ **Problem:**
176
+ In Typer, if a sub-app has both `@app.callback(invoke_without_command=True)` and `@app.command()` sub-commands, command routing becomes confused.
177
+
178
+ **Context:**
179
+ The `schema` command has two sub-commands `show` and `tables`, while `query`, `profile`, and other commands run directly (no sub-commands).
180
+
181
+ **Solution:**
182
+ - Direct-execution commands (`query`, `profile`, `slowlog`, `monitor`): use `@app.callback(invoke_without_command=True)`
183
+ - Multi-sub-command commands (`schema`, `migrate`): define sub-commands using standard `@app.command()`, without a callback
184
+
185
+ This keeps routing clean, and both `ch query "..."` and `ch schema show table_name` work correctly.
186
+
187
+ ---
188
+
189
+ ## 7. Rich `Live` and `Console` Compatibility
190
+
191
+ **Problem:**
192
+ In `ch monitor`, calling `console.print()` outside the `Live` context causes output to intermix with Live-refreshed content, resulting in a garbled terminal display.
193
+
194
+ **Solution:**
195
+ All content that needs to be shown during a Live refresh should be delivered by returning a new `Table` object via `live.update(new_table)`. Do not call `console.print()` directly inside a `Live` context.
196
+
197
+ ---
198
+
199
+ ## 8. `clickhouse-connect` Result Set Row Access
200
+
201
+ **Problem:**
202
+ `clickhouse-connect` query results support multiple access patterns that are easy to confuse:
203
+ - `result.result_rows` — returns `List[tuple]`, iterable row by row
204
+ - `result.first_row` — returns the first row as a `tuple` (note: raises `IndexError` if the result is empty)
205
+ - `result.row_count` — returns the number of rows
206
+
207
+ **Takeaway:**
208
+ Always check `result.row_count > 0` before accessing `result.first_row`, otherwise the command will crash when no query record exists (e.g., when `profile` cannot find the corresponding record in `query_log`).
209
+
210
+ ```python
211
+ if stats.row_count > 0:
212
+ row = stats.first_row
213
+ # safe access to row[0], row[1], ...
214
+ else:
215
+ # handle gracefully
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 9. Pydantic Settings `env_prefix` Behavior
221
+
222
+ **Problem:**
223
+ The configuration class uses `env_prefix = "CH_"`, meaning all environment variables must be prefixed with `CH_` (`CH_HOST`, `CH_PORT`, etc.). The `.env` file can sometimes be missing the prefix or use incorrect casing.
224
+
225
+ **Takeaway:**
226
+ - Pydantic Settings v2 reads environment variables case-insensitively by default
227
+ - Variables in the `.env` file must include the prefix (`CH_HOST=localhost`), not the bare field name (`HOST=localhost`)
228
+ - It is recommended to list all variables in `.env.example` to prevent users from missing any
229
+
230
+ ---
231
+
232
+ ## 10. Wheel Package Path Configuration in `pyproject.toml`
233
+
234
+ **Problem:**
235
+ `pyproject.toml` had `[tool.hatch.build.targets.wheel]` configured with `packages = ["src/clickhawk"]`, but the actual code structure places the code in `clickhawk/` (not `src/clickhawk/`), causing the package to not be found after `pip install`.
236
+
237
+ **Solution:**
238
+ Ensure the package path matches the actual directory structure. If the code is in `clickhawk/` (a same-name package at the project root), the configuration should be:
239
+ ```toml
240
+ [tool.hatch.build.targets.wheel]
241
+ packages = ["clickhawk"]
242
+ ```
243
+
244
+ **Takeaway:**
245
+ Before publishing to PyPI, always install with `pip install -e .` and run `ch --help` to verify that commands are registered correctly — do not rely solely on checking `pyproject.toml` syntax.
@@ -0,0 +1,245 @@
1
+ > English version: [LESSONS_LEARNED.md](LESSONS_LEARNED.md)
2
+
3
+ # 调试经验记录(Lessons Learned)
4
+
5
+ 开发 ClickHawk 过程中踩过的坑和学到的经验,供后续开发和贡献者参考。
6
+
7
+ ---
8
+
9
+ ## 1. macOS Gatekeeper 拦截 Homebrew tap 安装的 ClickHouse 二进制
10
+
11
+ **问题描述:**
12
+ 通过 `brew tap clickhouse/clickhouse && brew install clickhouse` 安装后,终端执行 `clickhouse` 报错"无法打开,因为无法验证开发者",或者直接被 macOS 静默拦截。
13
+
14
+ **根本原因:**
15
+ ClickHouse 官方 tap 安装的是预编译二进制,macOS Gatekeeper 会对未经 Apple 公证的二进制添加隔离标记(quarantine attribute),阻止其运行。
16
+
17
+ **解决方案:**
18
+ ```bash
19
+ xattr -d com.apple.quarantine $(which clickhouse)
20
+ ```
21
+
22
+ 执行后无需重启,立即生效。
23
+
24
+ **附:`brew services start clickhouse` 报错问题**
25
+ `clickhouse/clickhouse` tap 安装的是单文件二进制,没有注册 Homebrew service(没有 launchd plist),所以 `brew services` 无法管理它。
26
+ 正确的启动方式是直接运行(需提供完整 config.xml,见下一条):
27
+ ```bash
28
+ clickhouse server --config-file=$HOME/clickhouse-config.xml &
29
+ ```
30
+
31
+ **`ch slowlog` / `ch profile` 报 `Unknown table 'system.query_log'`**
32
+ config.xml 缺少 `<query_log>` 配置时,ClickHouse 不会创建 `system.query_log` 表(懒加载),导致 slowlog 和 profile 命令报错。解决方案:在 config.xml 中加入 `<query_log>` 节点并在 default profile 中设置 `<log_queries>1</log_queries>`。
33
+
34
+ **最小可用 config.xml 必须包含 profiles/users/quotas**,否则会报 `Settings profile 'default' not found`(exit 180)。仅有路径和端口配置不够,ClickHouse 启动时会查找 `default` profile:
35
+ ```xml
36
+ <clickhouse>
37
+ <path>...</path>
38
+ <tmp_path>...</tmp_path>
39
+ <tcp_port>9000</tcp_port>
40
+ <http_port>8123</http_port>
41
+ <listen_host>127.0.0.1</listen_host>
42
+ <profiles>
43
+ <default><max_memory_usage>10000000000</max_memory_usage></default>
44
+ </profiles>
45
+ <users>
46
+ <default>
47
+ <password></password>
48
+ <networks><ip>::/0</ip></networks>
49
+ <profile>default</profile>
50
+ <quota>default</quota>
51
+ </default>
52
+ </users>
53
+ <quotas>
54
+ <default>
55
+ <interval>
56
+ <duration>3600</duration>
57
+ <queries>0</queries><errors>0</errors>
58
+ <result_rows>0</result_rows><read_rows>0</read_rows>
59
+ <execution_time>0</execution_time>
60
+ </interval>
61
+ </default>
62
+ </quotas>
63
+ </clickhouse>
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 2. Typer 0.12+ 子 app 中选项不能放在位置参数之后
69
+
70
+ **问题描述:**
71
+ `ch query 'SELECT ...' --format json` 报 `Missing argument 'SQL'`,而 `ch query --format json 'SELECT ...'` 正常。
72
+
73
+ **根本原因:**
74
+ Typer 0.12+ 使用 `add_typer()` 嵌套子 app 时,子 app 的 callback 默认不允许选项(`--format`)出现在位置参数(SQL)之后,与用户直觉相反。
75
+
76
+ **解决方案:**
77
+ 在每个有位置参数 + 选项组合的子 app 上加 `context_settings={"allow_interspersed_args": True}`:
78
+ ```python
79
+ app = typer.Typer(help="...", context_settings={"allow_interspersed_args": True})
80
+ ```
81
+ 受影响的命令:`query`、`profile`、`slowlog`、`monitor`。
82
+
83
+ ---
84
+
85
+ ## 3. ClickHouse 26.x 在 macOS ARM64 上无配置文件启动崩溃
86
+
87
+ **问题描述:**
88
+ 在 macOS Apple Silicon 上执行 `./clickhouse server &`,服务端立即 crash 并 exit 91,错误信息为:
89
+
90
+ ```
91
+ DB::Exception: Poco::Exception. Code: 1000, e.code() = 0, Null pointer
92
+ DB::Context::setClustersConfig ... CANNOT_LOAD_CONFIG
93
+ ```
94
+
95
+ **根本原因:**
96
+ ClickHouse 26.3(及部分 26.x 版本)在 macOS ARM64 上以 embedded config(无 `config.xml`)模式启动时,`setClustersConfig` 内部触发 null pointer crash。这是 ClickHouse 的已知 bug,不是系统或权限问题。
97
+
98
+ **解决方案:**
99
+
100
+ 方案 1(推荐):使用 Docker,完全规避配置问题:
101
+ ```bash
102
+ docker run -d --name clickhouse-local \
103
+ -p 8123:8123 -p 9000:9000 \
104
+ --ulimit nofile=262144:262144 \
105
+ clickhouse/clickhouse-server
106
+ ```
107
+
108
+ 方案 2:提供最小 `config.xml` 绕过 bug:
109
+ ```bash
110
+ mkdir -p ~/clickhouse-data/{data,logs,tmp}
111
+ cat > config.xml << 'EOF'
112
+ <clickhouse>
113
+ <path>clickhouse-data/data/</path>
114
+ <tmp_path>clickhouse-data/tmp/</tmp_path>
115
+ <tcp_port>9000</tcp_port>
116
+ <http_port>8123</http_port>
117
+ <listen_host>127.0.0.1</listen_host>
118
+ </clickhouse>
119
+ EOF
120
+ ./clickhouse server --config-file=config.xml &
121
+ ```
122
+
123
+ **经验:**
124
+ 官方单文件二进制在 macOS 上不稳定(尤其是新版本),本地开发首选 Docker;Linux 上单文件模式通常没有此问题。
125
+
126
+ ---
127
+
128
+ ## 2. `system.query_log` 的延迟写入问题
129
+
130
+ **问题描述:**
131
+ 在 `ch profile` 命令中,执行完查询后立即查询 `system.query_log` 会得到空结果,导致性能统计不显示。
132
+
133
+ **根本原因:**
134
+ ClickHouse 的 `query_log` 不是同步写入的,查询结束后有一段异步刷新延迟(通常 100~500ms,取决于服务器负载和配置)。
135
+
136
+ **解决方案:**
137
+ 在执行完目标查询后,`time.sleep(0.3)` 等待 300ms 再查询 `system.query_log`。这是一个权衡:等待时间太短会漏掉数据,太长会影响用户体验。
138
+
139
+ ```python
140
+ client.query(sql, settings={"log_queries": 1, "query_id": query_id})
141
+ time.sleep(0.3) # 等待 query_log 异步刷新
142
+ stats = client.query(f"SELECT ... FROM system.query_log WHERE query_id = '{query_id}'")
143
+ ```
144
+
145
+ **经验:**
146
+ ClickHouse 的系统表(`system.query_log`、`system.part_log` 等)通常是异步写入的,不要假设它们是实时一致的。如果遇到空结果,优先考虑写入延迟问题。
147
+
148
+ ---
149
+
150
+ ## 2. `ProfileEvents` 字段访问方式
151
+
152
+ **问题描述:**
153
+ `system.query_log` 中的 `ProfileEvents` 是一个 `Map(String, UInt64)` 类型字段,访问特定 key 需要用 `ProfileEvents['KeyName']` 语法,而不是点号。
154
+
155
+ **正确写法:**
156
+ ```sql
157
+ SELECT
158
+ ProfileEvents['SelectedParts'] AS parts_selected,
159
+ ProfileEvents['SelectedRanges'] AS ranges_selected
160
+ FROM system.query_log
161
+ WHERE query_id = '...'
162
+ ```
163
+
164
+ **常用的 ProfileEvents key:**
165
+ - `SelectedParts` — 查询命中的 part 数量(越少越好,说明分区剪枝有效)
166
+ - `SelectedRanges` — 命中的 range 数量
167
+ - `SelectedMarks` — 命中的 mark 数量(与 index granularity 相关)
168
+ - `RealTimeMicroseconds` — 实际耗时(微秒)
169
+ - `UserTimeMicroseconds` — 用户态 CPU 耗时
170
+
171
+ ---
172
+
173
+ ## 3. Typer 子命令与 `callback` 的冲突
174
+
175
+ **问题描述:**
176
+ 在 Typer 中,如果一个子 app 既有 `@app.callback(invoke_without_command=True)` 又有 `@app.command()` 子命令,会出现命令路由混乱的问题。
177
+
178
+ **场景:**
179
+ `schema` 命令有两个子命令 `show` 和 `tables`,而 `query`、`profile` 等命令是直接运行的(没有子命令)。
180
+
181
+ **解决方案:**
182
+ - 直接运行型命令(`query`、`profile`、`slowlog`、`monitor`):使用 `@app.callback(invoke_without_command=True)`
183
+ - 多子命令型命令(`schema`、`migrate`):使用普通的 `@app.command()` 定义子命令,不用 callback
184
+
185
+ 这样路由清晰,`ch query "..."` 和 `ch schema show table_name` 都能正常工作。
186
+
187
+ ---
188
+
189
+ ## 4. Rich `Live` 与 `Console` 的兼容性
190
+
191
+ **问题描述:**
192
+ 在 `ch monitor` 中,如果在 `Live` 上下文之外调用 `console.print()`,输出会和 Live 刷新的内容混在一起,造成终端显示错乱。
193
+
194
+ **解决方案:**
195
+ 所有需要在 Live 刷新期间显示的内容,都通过返回新的 `Table` 对象来更新(`live.update(new_table)`),不要在 Live 上下文内直接调用 `console.print()`。
196
+
197
+ ---
198
+
199
+ ## 5. `clickhouse-connect` 结果集的行访问方式
200
+
201
+ **问题描述:**
202
+ `clickhouse-connect` 的查询结果有多种访问方式,容易搞混:
203
+ - `result.result_rows` — 返回 `List[tuple]`,逐行迭代
204
+ - `result.first_row` — 返回第一行的 `tuple`(注意:结果为空时会抛 `IndexError`)
205
+ - `result.row_count` — 返回行数
206
+
207
+ **经验:**
208
+ 在访问 `result.first_row` 之前,务必先检查 `result.row_count > 0`,否则在没有查询记录时(如 `profile` 命令在 `query_log` 中找不到对应记录时)会直接崩溃。
209
+
210
+ ```python
211
+ if stats.row_count > 0:
212
+ row = stats.first_row
213
+ # 安全访问 row[0], row[1], ...
214
+ else:
215
+ # 优雅降级处理
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 6. Pydantic Settings 的 `env_prefix` 行为
221
+
222
+ **问题描述:**
223
+ 配置类使用了 `env_prefix = "CH_"`,意味着所有环境变量需要加 `CH_` 前缀(`CH_HOST`、`CH_PORT` 等)。但 `.env` 文件里有时会漏掉前缀或者写错大小写。
224
+
225
+ **经验:**
226
+ - Pydantic Settings v2 默认不区分大小写读取环境变量
227
+ - `.env` 文件中的变量名需要包含前缀(`CH_HOST=localhost`),而不是裸字段名(`HOST=localhost`)
228
+ - 建议在 `.env.example` 中把所有变量都列出来,避免用户遗漏
229
+
230
+ ---
231
+
232
+ ## 7. `pyproject.toml` 中 wheel 打包路径的配置
233
+
234
+ **问题描述:**
235
+ `pyproject.toml` 中的 `[tool.hatch.build.targets.wheel]` 配置了 `packages = ["src/clickhawk"]`,但实际代码结构中代码在 `clickhawk/` 目录(而不是 `src/clickhawk/`),导致 `pip install` 后找不到包。
236
+
237
+ **解决方案:**
238
+ 确保打包路径与实际目录结构一致。如果代码在 `clickhawk/`(项目根目录下的同名包),则配置应为:
239
+ ```toml
240
+ [tool.hatch.build.targets.wheel]
241
+ packages = ["clickhawk"]
242
+ ```
243
+
244
+ **经验:**
245
+ 在发布 PyPI 之前,务必用 `pip install -e .` 安装后运行 `ch --help` 验证命令是否正常注册,而不只是检查 `pyproject.toml` 的语法。