@carllee1983/dbcli 1.9.1 → 1.10.1

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ All notable changes to dbcli are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.10.1] - 2026-05-08
9
+
10
+ ### Fixed
11
+
12
+ - **Packaged `dist/cli.mjs` 找不到 assets**:1.10.0 bundle 在 `task-paths.ts` / `snippet-paths.ts` 用 `import.meta.dir + ../../../` 解析 builtin 目錄,bundle 後三層往上會跳出 package root,npm 全域安裝的使用者執行 `dbcli queries list` / `dbcli skill tasks list` 讀不到資源。抽出 `src/utils/package-root.ts` 以 `package.json` 走訪定位 root,dev 與 bundle 都正確;`skill.ts` 內既有的 `findPackageRoot` 也收斂到同一處。
13
+ - **`dbcli q` 略過 blacklist 檢查(安全)**:`q.ts` 把空字串當作 `tableName` 傳給 `BlacklistValidator.filterColumns`,column-level redaction 永遠不命中;同時也沒呼叫 `checkTableBlacklist`,使用者可以透過 saved snippet 直接 SELECT 黑名單表/欄位繞開保護。改為從 `prepared.rewrittenSql` 抽出主表(SQL)或 `prepared.execHints.index`(ES),執行前先 `checkTableBlacklist('SELECT', target)`,並把真正的 `tableName` 餵給 `filterColumns`;Redis 維持原樣。
14
+
15
+ ### Added
16
+
17
+ - **dist/ 整合 smoke 測試**:`tests/integration/dist-smoke.test.ts` 從 OS tmpdir 執行 `dist/cli.mjs`,覆蓋 `--version`、`skill --output`、`queries list`、`skill tasks list`,守住 packaged assets path 不再回退。
18
+ - **`q` blacklist 迴歸測試**:`tests/unit/commands/q-blacklist.test.ts` 覆蓋黑名單表阻擋、欄位 redact、未受影響 snippet 三種情境。
19
+
20
+ ### Changed
21
+
22
+ - **Lint release-blocking**:`bun run lint` / `lint:fix` 加上 `--max-warnings=0`;同時清掉 45 個 `@typescript-eslint/no-explicit-any` warnings(以正型替代為主,`elasticsearch-adapter.ts` 因刻意不引入 `@elastic/elasticsearch` SDK 而以檔案層 `eslint-disable` 標註理由)。任何新 warning 從此會擋住 release。
23
+
24
+ ## [1.10.0] - 2026-05-08
25
+
26
+ ### Added
27
+
28
+ - **Saved Queries 擴展至 Elasticsearch 與 Redis**:`dbcli q @<name>` 與 `queries` 子命令現在能依 frontmatter `engine` 自動切換到對應引擎,並走各引擎專屬的安全管線。
29
+ - **Engine strategy 重構**:runner 透過 `EngineStrategy` 介面分派到 SQL / Elasticsearch / Redis 三個獨立 strategy;既有 SQL 行為以 strategy 形式保留,無行為變更。
30
+ - **Elasticsearch strategy**:
31
+ - Frontmatter 接受 `engine: elasticsearch` 與 `index` 欄位;body 必須是合法 JSON,含 `script` 欄位的 query 直接拒絕。
32
+ - JSON-aware 參數注入:`:name` 僅在 JSON 字串脈絡裡替換,避免破壞語法。
33
+ - Size guard:自動補 `size` 上限;`aggs` 模式下放行但加註警告,分頁 (`from + size`) 過大時提示。
34
+ - **Redis strategy**:
35
+ - 命令白名單(read-only 為主)+ body validation;直接拒絕 unsupported 或寫入命令。
36
+ - Raw 參數注入:`:name` 直接代入字面量並打印 foot-gun 警告,提醒使用者 saved query 內不可放使用者輸入。
37
+ - Size guard:對 range / SCAN 命令的 `COUNT` / `LIMIT` 加上保險上限。
38
+ - **`q` 命令分派**:根據 prepared execution 的 engine family 呼叫對應 adapter,`--dry-run` 依 engine 用對應格式輸出(SQL 維持 SQL、ES 印 JSON body、Redis 印 argv)。
39
+ - **內建診斷 snippet**:
40
+ - `assets/snippets/diag/es-cluster-health.elasticsearch.sql` — ES 叢集健康度摘要。
41
+ - `assets/snippets/diag/redis-key-stats.redis.sql` — Redis key 數量 / type 分佈快照。
42
+ - **整合測試**:新增 ES / Redis end-to-end saved query 測試(依本機是否有 Docker 而 skip,與既有 PG / MySQL 測試一致)。
43
+
44
+ ### Changed
45
+
46
+ - **Redis 驅動**:改用 Bun 內建 `RedisClient`,移除外部 `ioredis` 依賴。
47
+ - **Elasticsearch adapter**:refactor 並收斂錯誤訊息與 ExecutionResult 形狀,與 SQL / Mongo / Redis 對齊。
48
+ - **文件**:`assets/SKILL.md` 與 `assets/reference.md` 補上 ES / Redis snippet 工作流;`docs/feature-matrix.md` 更新 saved-queries 欄位。
49
+
50
+ ### Fixed
51
+
52
+ - **`dbcli export`(Redis 分支)**:`result.rowCount` 在 Redis 上可能 undefined 時導致 `tsc --noEmit` 報 TS2322;改為 `result.rowCount ?? result.rows.length ?? 0`,release gate 中的 typecheck 回到 0 錯誤。
53
+
8
54
  ## [1.9.1] - 2026-05-07
9
55
 
10
56
  ### Changed
package/assets/SKILL.md CHANGED
@@ -237,6 +237,18 @@ Each `.sql` file may declare YAML frontmatter inside `-- ---` blocks
237
237
  (name, description, engine, params, tags). See `dbcli queries show @<name> --format json`
238
238
  for the machine-readable contract.
239
239
 
240
+ ### Engine-specific bodies
241
+
242
+ Each snippet's body format is determined by the `engine` frontmatter field:
243
+
244
+ | Engine | Body format | Notes |
245
+ |-------------------|------------------------|-------|
246
+ | postgres / mysql | Single SELECT or WITH | `:name` → driver bind (`$1` / `?`) |
247
+ | elasticsearch | JSON DSL | `:name` → JSON-aware substitution; `index:` field required |
248
+ | redis | Single Redis command | `:name` → raw text; only read commands allowed |
249
+
250
+ Mixed-family `engine` arrays (e.g. `[postgres, elasticsearch]`) are rejected at parse time.
251
+
240
252
  ### Built-in diagnostic snippets
241
253
 
242
254
  dbcli ships ready-made diagnostic queries. Run with `dbcli q @diag/<topic>`:
@@ -251,6 +263,8 @@ dbcli ships ready-made diagnostic queries. Run with `dbcli q @diag/<topic>`:
251
263
  | `@diag/locks` | lock-wait chains |
252
264
  | `@diag/db-size` | database size summary |
253
265
  | `@diag/cache-hit` | buffer cache hit ratios |
266
+ | `@diag/es-cluster-health` | document counts per index (ES connections) |
267
+ | `@diag/redis-key-stats` | sample SCAN over keyspace (Redis connections) |
254
268
 
255
269
  Engine variants are picked automatically based on the active connection.
256
270
  Override any of them by placing a same-named file under `.dbcli-shared/queries/`
@@ -230,6 +230,62 @@ Param placeholders use `:name`. They are rewritten to `$1, $2, …` (Postgres) o
230
230
  - Files exceeding 64 KiB are rejected.
231
231
  - `--no-limit` is honoured only at the outermost level; nested subqueries are still wrapped by the size guard.
232
232
 
233
+ ##### Elasticsearch snippets
234
+
235
+ Body is a JSON DSL `_search` request body. Frontmatter requires an `index:` field (may contain `:param`).
236
+
237
+ Example:
238
+
239
+ -- ---
240
+ -- name: events-by-day
241
+ -- engine: elasticsearch
242
+ -- index: 'events-:date'
243
+ -- params:
244
+ -- date: { type: date, required: true }
245
+ -- user_id: { type: int, required: true }
246
+ -- ---
247
+ {
248
+ "query": {
249
+ "bool": {
250
+ "filter": [
251
+ { "term": { "user_id": :user_id } }
252
+ ]
253
+ }
254
+ },
255
+ "size": 100
256
+ }
257
+
258
+ Substitution rules (type-aware JSON injection):
259
+
260
+ - `int` / `float` / `bool` outside string literals → bare value (`42`, `1.5`, `true`)
261
+ - `string` / `date` / `datetime` outside string literals → JSON-quoted (`"Alice"`, `"2026-05-08"`)
262
+ - Any param inside a JSON string literal → escaped inner form (`"prefix-:name"` works)
263
+
264
+ `script` and `script_fields` are rejected anywhere in the body.
265
+
266
+ Size guard: if `size` is missing, `1000` is injected (or `0` when `aggs` is present); explicit `size > 1000` is overridden with a warning unless `--no-limit`.
267
+
268
+ ##### Redis snippets
269
+
270
+ Body is a single Redis command on one line. Only read-only commands are allowed:
271
+ `GET MGET HGET HGETALL HMGET HKEYS HVALS HLEN HEXISTS LRANGE LLEN LINDEX SMEMBERS SISMEMBER SCARD ZRANGE ZRANGEBYSCORE ZRANGEBYLEX ZSCORE ZCARD ZCOUNT ZRANK TYPE EXISTS TTL PTTL STRLEN OBJECT SCAN HSCAN SSCAN ZSCAN`.
272
+
273
+ `KEYS`, `EVAL`, `FLUSHDB`, `FLUSHALL`, `CONFIG`, `DEBUG`, `SHUTDOWN`, `SCRIPT` and any write command are rejected.
274
+
275
+ Example:
276
+
277
+ -- ---
278
+ -- name: cache-user
279
+ -- engine: redis
280
+ -- params:
281
+ -- id: { type: int, required: true }
282
+ -- ---
283
+ HGETALL user::id
284
+
285
+ Substitution rules: pure raw text — `:name` becomes the value's `String()` form. **Quoting is the snippet author's responsibility**: wrap `:name` in double quotes if the value may contain whitespace. The parser warns when a `string`-typed `:name` is adjacent to non-whitespace and unquoted.
286
+
287
+ Size guard: `LRANGE` / `ZRANGE` stop overridden when `< 0` or `> 1000`; `SCAN` / `HSCAN` / `SSCAN` / `ZSCAN` get `COUNT 1000` injected if absent. `--no-limit` disables.
288
+
233
289
  ### queries
234
290
 
235
291
  Manage saved snippets — discover, inspect, scaffold, and edit local copies. Mutating
@@ -582,7 +638,9 @@ Redis connections speak Redis commands rather than SQL. The adapter uses the `io
582
638
 
583
639
  **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `status`, `doctor`, `upgrade`, `completion`
584
640
 
585
- **Not supported (exit with error or unsupported error):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `q` (saved queries), `shell`. For writes, run the equivalent Redis command via `query` — the same permission gate applies.
641
+ **Saved queries:** `q` is supported for read-only Redis commands (see "Redis snippets" below).
642
+
643
+ **Not supported (exit with error or unsupported error):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `shell`. For writes, run the equivalent Redis command via `query` — the same permission gate applies.
586
644
 
587
645
  ### Connection and configuration
588
646
 
@@ -648,7 +706,9 @@ Elasticsearch connections speak the REST API. The adapter is fetch-based (no SDK
648
706
 
649
707
  **Supported commands:** `init`, `use`, `list`, `schema`, `query`, `status`, `doctor`, `upgrade`, `completion`
650
708
 
651
- **Not supported (use external tooling):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `q`, `shell`. The permission classifier already understands `_doc` / `_update` / `_bulk` so future write surfaces can be wired in without changing tiers.
709
+ **Saved queries:** `q` is supported for ES JSON DSL bodies (see "Elasticsearch snippets" below).
710
+
711
+ **Not supported (use external tooling):** `insert`, `update`, `delete`, `export`, `check`, `diff`, `migrate`, `shell`. The permission classifier already understands `_doc` / `_update` / `_bulk` so future write surfaces can be wired in without changing tiers.
652
712
 
653
713
  ### Connection and configuration
654
714
 
@@ -0,0 +1,15 @@
1
+ -- ---
2
+ -- name: es-cluster-health
3
+ -- description: Document counts per index across the cluster
4
+ -- engine: elasticsearch
5
+ -- index: '*'
6
+ -- tags: [diag, elasticsearch]
7
+ -- ---
8
+ {
9
+ "size": 0,
10
+ "aggs": {
11
+ "by_index": {
12
+ "terms": { "field": "_index", "size": 50 }
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,11 @@
1
+ -- ---
2
+ -- name: redis-key-stats
3
+ -- description: Sample keyspace via SCAN with explicit COUNT bound
4
+ -- engine: redis
5
+ -- params:
6
+ -- match:
7
+ -- type: string
8
+ -- default: '*'
9
+ -- tags: [diag, redis]
10
+ -- ---
11
+ SCAN 0 MATCH :match COUNT 100