@carllee1983/dbcli 1.48.0 → 1.50.0
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -6
- package/.cursor/rules/dbcli.mdc +1 -1
- package/.cursor/skills/dbcli/reference.md +11 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/skills/dbcli/SKILL.md +1 -1
- package/.github/skills/dbcli/reference.md +11 -1
- package/CHANGELOG.md +38 -0
- package/README.md +3 -0
- package/assets/SKILL.md +1 -1
- package/assets/SKILL.zh-TW.md +1 -1
- package/assets/reference.md +11 -1
- package/assets/ui-template.html +10 -10
- package/dist/cli.mjs +250 -38
- package/dist/core.mjs +88 -9
- package/gemini-extension.json +1 -1
- package/package.json +4 -3
- package/plugins/dbcli-agent/.codex-plugin/plugin.json +2 -6
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +1 -1
- package/plugins/dbcli-agent/skills/dbcli/reference.md +11 -1
- package/skills/dbcli/SKILL.md +1 -1
- package/skills/dbcli/reference.md +11 -1
package/dist/cli.mjs
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@carllee1983/dbcli",
|
|
55
|
-
version: "1.
|
|
55
|
+
version: "1.50.0",
|
|
56
56
|
description: "Database CLI for AI agents",
|
|
57
57
|
type: "module",
|
|
58
58
|
publishConfig: {
|
|
@@ -133,8 +133,9 @@ var init_package = __esm(() => {
|
|
|
133
133
|
typecheck: "tsc --noEmit --pretty false",
|
|
134
134
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
135
135
|
lint: "eslint src tests scripts --ext .ts --max-warnings=0",
|
|
136
|
-
"
|
|
137
|
-
format:
|
|
136
|
+
"format:check": "prettier --check .",
|
|
137
|
+
format: "prettier --write .",
|
|
138
|
+
"lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0"
|
|
138
139
|
},
|
|
139
140
|
dependencies: {
|
|
140
141
|
"cli-table3": "^0.6.5",
|
|
@@ -30311,17 +30312,37 @@ function projectRows(rows, selection) {
|
|
|
30311
30312
|
const columnNames = collectColumnNames(projectedRows);
|
|
30312
30313
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
30313
30314
|
}
|
|
30314
|
-
function hasFieldPath(row,
|
|
30315
|
-
return readPath(row,
|
|
30315
|
+
function hasFieldPath(row, segments) {
|
|
30316
|
+
return readPath(row, segments).found;
|
|
30316
30317
|
}
|
|
30317
30318
|
function omitFieldPaths(rows, paths) {
|
|
30318
|
-
|
|
30319
|
-
|
|
30320
|
-
|
|
30321
|
-
|
|
30319
|
+
const topLevel = new Set(paths);
|
|
30320
|
+
const dotted = paths.filter((path5) => path5.includes(".")).map((path5) => ({ head: path5.slice(0, path5.indexOf(".")), segments: path5.split(".") }));
|
|
30321
|
+
const maskRecord = (row) => {
|
|
30322
|
+
let projected = cloneRecord(row, topLevel);
|
|
30323
|
+
for (const { head, segments } of dotted) {
|
|
30324
|
+
const value = row[head];
|
|
30325
|
+
if (value !== null && typeof value === "object")
|
|
30326
|
+
projected = omitPath(projected, segments);
|
|
30327
|
+
}
|
|
30322
30328
|
return projected;
|
|
30329
|
+
};
|
|
30330
|
+
return rows.map((row) => {
|
|
30331
|
+
if (row === null || typeof row !== "object")
|
|
30332
|
+
return {};
|
|
30333
|
+
if (Array.isArray(row))
|
|
30334
|
+
return maskArrayRow(row, maskRecord);
|
|
30335
|
+
return maskRecord(row);
|
|
30323
30336
|
});
|
|
30324
30337
|
}
|
|
30338
|
+
function maskArrayRow(row, maskRecord) {
|
|
30339
|
+
const out = {};
|
|
30340
|
+
row.forEach((item, index) => {
|
|
30341
|
+
const masked = Array.isArray(item) ? maskArrayRow(item, maskRecord) : item !== null && typeof item === "object" ? maskRecord(item) : item;
|
|
30342
|
+
defineData(out, String(index), masked);
|
|
30343
|
+
});
|
|
30344
|
+
return out;
|
|
30345
|
+
}
|
|
30325
30346
|
function toMongoProjection(selection) {
|
|
30326
30347
|
const projection = {};
|
|
30327
30348
|
const value = selection.mode === "include" ? 1 : 0;
|
|
@@ -30384,10 +30405,10 @@ function omitPath(value, segments) {
|
|
|
30384
30405
|
}
|
|
30385
30406
|
return out;
|
|
30386
30407
|
}
|
|
30387
|
-
function cloneRecord(value,
|
|
30408
|
+
function cloneRecord(value, omittedKeys) {
|
|
30388
30409
|
const out = {};
|
|
30389
30410
|
for (const [key, child] of Object.entries(value)) {
|
|
30390
|
-
if (key
|
|
30411
|
+
if (!omittedKeys.has(key))
|
|
30391
30412
|
defineData(out, key, child);
|
|
30392
30413
|
}
|
|
30393
30414
|
return out;
|
|
@@ -30526,6 +30547,16 @@ var MAX_DECODE_PASSES = 4;
|
|
|
30526
30547
|
var init_es_index_target = () => {};
|
|
30527
30548
|
|
|
30528
30549
|
// src/core/blacklist-validator.ts
|
|
30550
|
+
function flattenArrayRow(row) {
|
|
30551
|
+
const records = [];
|
|
30552
|
+
for (const item of row) {
|
|
30553
|
+
if (Array.isArray(item))
|
|
30554
|
+
records.push(...flattenArrayRow(item));
|
|
30555
|
+
else if (item !== null && typeof item === "object")
|
|
30556
|
+
records.push(item);
|
|
30557
|
+
}
|
|
30558
|
+
return records;
|
|
30559
|
+
}
|
|
30529
30560
|
function dedupe2(values) {
|
|
30530
30561
|
const seen = new Set;
|
|
30531
30562
|
const result = [];
|
|
@@ -30626,7 +30657,56 @@ class BlacklistValidator {
|
|
|
30626
30657
|
if (blacklistedColumns.length === 0) {
|
|
30627
30658
|
return { filteredRows: rows, omittedColumns: [] };
|
|
30628
30659
|
}
|
|
30629
|
-
const
|
|
30660
|
+
const probeNested = blacklistedColumns.some((path5) => path5.includes("."));
|
|
30661
|
+
const presentColumns = new Set(columnList);
|
|
30662
|
+
const nestedHeads = new Set;
|
|
30663
|
+
const collect2 = (record) => {
|
|
30664
|
+
for (const key of Object.getOwnPropertyNames(record)) {
|
|
30665
|
+
presentColumns.add(key);
|
|
30666
|
+
if (!probeNested)
|
|
30667
|
+
continue;
|
|
30668
|
+
const value = record[key];
|
|
30669
|
+
if (value !== null && typeof value === "object")
|
|
30670
|
+
nestedHeads.add(key);
|
|
30671
|
+
}
|
|
30672
|
+
};
|
|
30673
|
+
for (const row of rows) {
|
|
30674
|
+
if (row === null || typeof row !== "object")
|
|
30675
|
+
continue;
|
|
30676
|
+
if (Array.isArray(row)) {
|
|
30677
|
+
for (const item of flattenArrayRow(row))
|
|
30678
|
+
collect2(item);
|
|
30679
|
+
continue;
|
|
30680
|
+
}
|
|
30681
|
+
collect2(row);
|
|
30682
|
+
}
|
|
30683
|
+
const protectedPaths = new Set(blacklistedColumns);
|
|
30684
|
+
const omitted = new Set;
|
|
30685
|
+
for (const path5 of blacklistedColumns) {
|
|
30686
|
+
if (presentColumns.has(path5)) {
|
|
30687
|
+
omitted.add(path5);
|
|
30688
|
+
continue;
|
|
30689
|
+
}
|
|
30690
|
+
const dot = path5.indexOf(".");
|
|
30691
|
+
if (dot < 0)
|
|
30692
|
+
continue;
|
|
30693
|
+
if (!nestedHeads.has(path5.slice(0, dot)))
|
|
30694
|
+
continue;
|
|
30695
|
+
const segments = path5.split(".");
|
|
30696
|
+
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
30697
|
+
omitted.add(path5);
|
|
30698
|
+
}
|
|
30699
|
+
for (const column of presentColumns) {
|
|
30700
|
+
let dot = column.indexOf(".");
|
|
30701
|
+
while (dot >= 0) {
|
|
30702
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
30703
|
+
omitted.add(column);
|
|
30704
|
+
break;
|
|
30705
|
+
}
|
|
30706
|
+
dot = column.indexOf(".", dot + 1);
|
|
30707
|
+
}
|
|
30708
|
+
}
|
|
30709
|
+
const omittedColumns = Array.from(omitted);
|
|
30630
30710
|
if (omittedColumns.length === 0) {
|
|
30631
30711
|
return { filteredRows: rows, omittedColumns: [] };
|
|
30632
30712
|
}
|
|
@@ -106230,6 +106310,10 @@ function fingerprintSql(sql) {
|
|
|
106230
106310
|
function shellEscapeDq(s) {
|
|
106231
106311
|
return s.replace(/\\/g, "\\\\").replace(/\$/g, "\\$").replace(/`/g, "\\`").replace(/"/g, "\\\"");
|
|
106232
106312
|
}
|
|
106313
|
+
function explainSuggestions(sql) {
|
|
106314
|
+
const esc = shellEscapeDq(sql);
|
|
106315
|
+
return [`dbcli explain "${esc}"`, `dbcli guide missing-index-for "${esc}"`];
|
|
106316
|
+
}
|
|
106233
106317
|
function buildByFingerprint(events, slowMs, top) {
|
|
106234
106318
|
const errorByFp = new Map;
|
|
106235
106319
|
for (const e of events.filter(isErrored)) {
|
|
@@ -106298,11 +106382,7 @@ function buildByFingerprint(events, slowMs, top) {
|
|
|
106298
106382
|
stats.sort((a, b) => b.durationMs.total - a.durationMs.total);
|
|
106299
106383
|
return stats.map((s, i) => {
|
|
106300
106384
|
if (i < top && s.statement === "SELECT") {
|
|
106301
|
-
|
|
106302
|
-
return {
|
|
106303
|
-
...s,
|
|
106304
|
-
suggestedCommands: [`dbcli explain "${sql}"`, `dbcli guide missing-index-for "${sql}"`]
|
|
106305
|
-
};
|
|
106385
|
+
return { ...s, suggestedCommands: explainSuggestions(s.exampleSql) };
|
|
106306
106386
|
}
|
|
106307
106387
|
return s;
|
|
106308
106388
|
});
|
|
@@ -106329,13 +106409,27 @@ function buildErrors(events) {
|
|
|
106329
106409
|
message: e.error.message,
|
|
106330
106410
|
count: 0,
|
|
106331
106411
|
fingerprint: fingerprintSql(e.sql),
|
|
106332
|
-
exampleSql: e.sql
|
|
106412
|
+
exampleSql: e.sql,
|
|
106413
|
+
tables: e.tables
|
|
106333
106414
|
};
|
|
106334
106415
|
groups.set(key, g);
|
|
106335
106416
|
}
|
|
106336
106417
|
g.count += 1;
|
|
106337
106418
|
}
|
|
106338
|
-
return [...groups.values()].sort((a, b) => b.count - a.count)
|
|
106419
|
+
return [...groups.values()].sort((a, b) => b.count - a.count).map((g) => {
|
|
106420
|
+
const schemaCmds = g.tables.slice(0, MAX_ERROR_SCHEMA_TABLES).map((t2) => `dbcli schema ${t2}`);
|
|
106421
|
+
const hint = `failed ${g.count}\xD7: verify the involved table/column names against the live schema ` + `before fixing the SQL \u2014 never guess column names`;
|
|
106422
|
+
return {
|
|
106423
|
+
code: g.code,
|
|
106424
|
+
message: g.message,
|
|
106425
|
+
count: g.count,
|
|
106426
|
+
fingerprint: g.fingerprint,
|
|
106427
|
+
exampleSql: g.exampleSql,
|
|
106428
|
+
tables: g.tables,
|
|
106429
|
+
...schemaCmds.length ? { suggestedCommands: schemaCmds } : {},
|
|
106430
|
+
hints: [hint]
|
|
106431
|
+
};
|
|
106432
|
+
});
|
|
106339
106433
|
}
|
|
106340
106434
|
function buildHotTables(events) {
|
|
106341
106435
|
const map = new Map;
|
|
@@ -106364,10 +106458,13 @@ function buildRepetition(events, threshold) {
|
|
|
106364
106458
|
fingerprint: fp,
|
|
106365
106459
|
sessionId: e.sessionId,
|
|
106366
106460
|
tables: e.tables,
|
|
106461
|
+
statement: e.statement,
|
|
106367
106462
|
count: 0,
|
|
106368
106463
|
totalDurationMs: 0,
|
|
106369
106464
|
minTs: ts,
|
|
106370
|
-
maxTs: ts
|
|
106465
|
+
maxTs: ts,
|
|
106466
|
+
exampleSql: e.sql,
|
|
106467
|
+
exampleDuration: e.durationMs
|
|
106371
106468
|
};
|
|
106372
106469
|
groups.set(key, g);
|
|
106373
106470
|
}
|
|
@@ -106377,15 +106474,27 @@ function buildRepetition(events, threshold) {
|
|
|
106377
106474
|
g.minTs = ts;
|
|
106378
106475
|
if (ts > g.maxTs)
|
|
106379
106476
|
g.maxTs = ts;
|
|
106477
|
+
if (e.durationMs > g.exampleDuration) {
|
|
106478
|
+
g.exampleDuration = e.durationMs;
|
|
106479
|
+
g.exampleSql = e.sql;
|
|
106480
|
+
}
|
|
106380
106481
|
}
|
|
106381
|
-
return [...groups.values()].filter((g) => g.count >= threshold).map((g) =>
|
|
106382
|
-
|
|
106383
|
-
|
|
106384
|
-
|
|
106385
|
-
|
|
106386
|
-
|
|
106387
|
-
|
|
106388
|
-
|
|
106482
|
+
return [...groups.values()].filter((g) => g.count >= threshold).sort((a, b) => b.count - a.count).map((g) => {
|
|
106483
|
+
const spanMs = g.maxTs - g.minTs;
|
|
106484
|
+
const hint = `N+1 suspect: the same query ran ${g.count}\xD7 within one session over ${spanMs}ms \u2014 ` + `collapse into a single query (JOIN / IN (...)) or cache the result`;
|
|
106485
|
+
return {
|
|
106486
|
+
fingerprint: g.fingerprint,
|
|
106487
|
+
sessionId: g.sessionId,
|
|
106488
|
+
count: g.count,
|
|
106489
|
+
spanMs,
|
|
106490
|
+
totalDurationMs: g.totalDurationMs,
|
|
106491
|
+
tables: g.tables,
|
|
106492
|
+
statement: g.statement,
|
|
106493
|
+
exampleSql: g.exampleSql,
|
|
106494
|
+
...g.statement === "SELECT" ? { suggestedCommands: explainSuggestions(g.exampleSql) } : {},
|
|
106495
|
+
hints: [hint]
|
|
106496
|
+
};
|
|
106497
|
+
});
|
|
106389
106498
|
}
|
|
106390
106499
|
function analyzeEvents(events, opts) {
|
|
106391
106500
|
const timestamps = events.map((e) => e.timestamp).filter(Boolean).sort();
|
|
@@ -106439,7 +106548,7 @@ function buildSummary(events, slowMs) {
|
|
|
106439
106548
|
}
|
|
106440
106549
|
};
|
|
106441
106550
|
}
|
|
106442
|
-
var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored";
|
|
106551
|
+
var isCompleted = (e) => e.type === "query_completed", isErrored = (e) => e.type === "query_errored", MAX_ERROR_SCHEMA_TABLES = 3;
|
|
106443
106552
|
var init_analyze = __esm(() => {
|
|
106444
106553
|
init_sql_metadata();
|
|
106445
106554
|
});
|
|
@@ -106479,16 +106588,113 @@ function renderAnalysisText(report, top) {
|
|
|
106479
106588
|
for (const r of report.repetition.slice(0, top)) {
|
|
106480
106589
|
L2.push(` ${r.count}x in session ${r.sessionId} (${r.spanMs}ms) ${r.fingerprint}`);
|
|
106481
106590
|
}
|
|
106482
|
-
const cmds = [
|
|
106591
|
+
const cmds = [
|
|
106592
|
+
...new Set([
|
|
106593
|
+
...report.byFingerprint.flatMap((f) => f.suggestedCommands ?? []),
|
|
106594
|
+
...report.errors.flatMap((e) => e.suggestedCommands ?? []),
|
|
106595
|
+
...report.repetition.flatMap((r) => r.suggestedCommands ?? [])
|
|
106596
|
+
])
|
|
106597
|
+
];
|
|
106483
106598
|
if (cmds.length) {
|
|
106484
106599
|
L2.push("", "SUGGESTED COMMANDS");
|
|
106485
106600
|
for (const c2 of cmds)
|
|
106486
106601
|
L2.push(` ${c2}`);
|
|
106487
106602
|
}
|
|
106603
|
+
const hints = [
|
|
106604
|
+
...new Set([
|
|
106605
|
+
...report.errors.flatMap((e) => e.hints ?? []),
|
|
106606
|
+
...report.repetition.flatMap((r) => r.hints ?? [])
|
|
106607
|
+
])
|
|
106608
|
+
];
|
|
106609
|
+
if (hints.length) {
|
|
106610
|
+
L2.push("", "HINTS");
|
|
106611
|
+
for (const h of hints)
|
|
106612
|
+
L2.push(` ${h}`);
|
|
106613
|
+
}
|
|
106488
106614
|
return L2.join(`
|
|
106489
106615
|
`);
|
|
106490
106616
|
}
|
|
106491
106617
|
|
|
106618
|
+
// src/querylens/analyze.ts
|
|
106619
|
+
function hasSql2(event) {
|
|
106620
|
+
return event.type === "query_observed" || event.type === "query_completed" || event.type === "query_errored";
|
|
106621
|
+
}
|
|
106622
|
+
function redactEventsForAnalysis(events) {
|
|
106623
|
+
return events.map((event) => {
|
|
106624
|
+
if (event.type === "query_errored") {
|
|
106625
|
+
return {
|
|
106626
|
+
...event,
|
|
106627
|
+
sql: redactLiterals(event.sql),
|
|
106628
|
+
error: { ...event.error, message: redactLiterals(event.error.message) }
|
|
106629
|
+
};
|
|
106630
|
+
}
|
|
106631
|
+
if (hasSql2(event))
|
|
106632
|
+
return { ...event, sql: redactLiterals(event.sql) };
|
|
106633
|
+
return event;
|
|
106634
|
+
});
|
|
106635
|
+
}
|
|
106636
|
+
function analyzeQuerylensEvents(events, opts) {
|
|
106637
|
+
return {
|
|
106638
|
+
...analyzeEvents(redactEventsForAnalysis(events), opts),
|
|
106639
|
+
querylens: { name: "querylens", version: QUERYLENS_VERSION }
|
|
106640
|
+
};
|
|
106641
|
+
}
|
|
106642
|
+
var QUERYLENS_VERSION = "0.1.0";
|
|
106643
|
+
var init_analyze2 = __esm(() => {
|
|
106644
|
+
init_analyze();
|
|
106645
|
+
init_sql_metadata();
|
|
106646
|
+
});
|
|
106647
|
+
|
|
106648
|
+
// src/querylens/render.ts
|
|
106649
|
+
function sqlInline(sql) {
|
|
106650
|
+
return sql.replace(/\s+/g, " ").trim().replace(/`/g, "\\`");
|
|
106651
|
+
}
|
|
106652
|
+
function none(lines, values) {
|
|
106653
|
+
if (values.length === 0)
|
|
106654
|
+
lines.push("_None._");
|
|
106655
|
+
}
|
|
106656
|
+
function renderQuerylensMarkdown(report, top) {
|
|
106657
|
+
const lines = [
|
|
106658
|
+
"# QueryLens report",
|
|
106659
|
+
"",
|
|
106660
|
+
"## Summary",
|
|
106661
|
+
"",
|
|
106662
|
+
`- Sessions: ${report.summary.sessions}`,
|
|
106663
|
+
`- Queries: ${report.summary.queries}`,
|
|
106664
|
+
`- Errors: ${report.summary.errors} (${(report.summary.errorRate * 100).toFixed(2)}%)`,
|
|
106665
|
+
`- Slow queries: ${report.summary.slowCount}`,
|
|
106666
|
+
`- Latency: p50 ${report.summary.latencyMs.p50}ms, p95 ${report.summary.latencyMs.p95}ms, p99 ${report.summary.latencyMs.p99}ms, max ${report.summary.latencyMs.max}ms`,
|
|
106667
|
+
"",
|
|
106668
|
+
"## Top expensive fingerprints",
|
|
106669
|
+
""
|
|
106670
|
+
];
|
|
106671
|
+
const fingerprints = report.byFingerprint.slice(0, top);
|
|
106672
|
+
none(lines, fingerprints);
|
|
106673
|
+
for (const item of fingerprints) {
|
|
106674
|
+
lines.push(`- ${item.count}\xD7 \xB7 ${item.durationMs.total}ms total \xB7 ${item.durationMs.avg}ms avg \xB7 \`${sqlInline(item.fingerprint)}\``);
|
|
106675
|
+
}
|
|
106676
|
+
lines.push("", "## Slowest queries", "");
|
|
106677
|
+
const slowest = report.slowest.slice(0, top);
|
|
106678
|
+
none(lines, slowest);
|
|
106679
|
+
for (const query of slowest)
|
|
106680
|
+
lines.push(`- ${query.durationMs}ms \xB7 \`${sqlInline(query.sql)}\``);
|
|
106681
|
+
lines.push("", "## Errors", "");
|
|
106682
|
+
const errors3 = report.errors.slice(0, top);
|
|
106683
|
+
none(lines, errors3);
|
|
106684
|
+
for (const error of errors3) {
|
|
106685
|
+
lines.push(`- ${error.count}\xD7 \xB7 [${error.code ?? "?"}] ${error.message} \xB7 \`${sqlInline(error.fingerprint)}\``);
|
|
106686
|
+
}
|
|
106687
|
+
lines.push("", "## N+1 suspects", "");
|
|
106688
|
+
const repetition = report.repetition.slice(0, top);
|
|
106689
|
+
none(lines, repetition);
|
|
106690
|
+
for (const item of repetition) {
|
|
106691
|
+
lines.push(`- ${item.count}\xD7 in session ${item.sessionId} over ${item.spanMs}ms \xB7 \`${sqlInline(item.fingerprint)}\``);
|
|
106692
|
+
}
|
|
106693
|
+
return `${lines.join(`
|
|
106694
|
+
`)}
|
|
106695
|
+
`;
|
|
106696
|
+
}
|
|
106697
|
+
|
|
106492
106698
|
// src/commands/proxy.ts
|
|
106493
106699
|
import { join as join38 } from "path";
|
|
106494
106700
|
function parseHostPort(value) {
|
|
@@ -106627,19 +106833,24 @@ async function runAnalyze(options) {
|
|
|
106627
106833
|
if (files.length === 0) {
|
|
106628
106834
|
throw new Error(`no events found at ${eventsPath}; run 'dbcli proxy <engine>' first`);
|
|
106629
106835
|
}
|
|
106630
|
-
const
|
|
106836
|
+
const analyzeOptions = {
|
|
106631
106837
|
slowMs,
|
|
106632
106838
|
top,
|
|
106633
106839
|
nPlusOne,
|
|
106634
106840
|
sourceFiles: files,
|
|
106635
106841
|
malformedLines
|
|
106636
|
-
}
|
|
106637
|
-
if (format === "
|
|
106638
|
-
process.stdout.write(
|
|
106639
|
-
`);
|
|
106842
|
+
};
|
|
106843
|
+
if (format === "markdown") {
|
|
106844
|
+
process.stdout.write(renderQuerylensMarkdown(analyzeQuerylensEvents(events, analyzeOptions), top));
|
|
106640
106845
|
} else {
|
|
106641
|
-
|
|
106846
|
+
const report = analyzeEvents(events, analyzeOptions);
|
|
106847
|
+
if (format === "text") {
|
|
106848
|
+
process.stdout.write(renderAnalysisText(report, top) + `
|
|
106642
106849
|
`);
|
|
106850
|
+
} else {
|
|
106851
|
+
process.stdout.write(JSON.stringify(report, null, 2) + `
|
|
106852
|
+
`);
|
|
106853
|
+
}
|
|
106643
106854
|
}
|
|
106644
106855
|
} catch (error) {
|
|
106645
106856
|
if (error instanceof Error)
|
|
@@ -106656,6 +106867,7 @@ var init_proxy = __esm(() => {
|
|
|
106656
106867
|
init_server();
|
|
106657
106868
|
init_event_reader();
|
|
106658
106869
|
init_analyze();
|
|
106870
|
+
init_analyze2();
|
|
106659
106871
|
SUPPORTED = ["mysql", "mariadb", "postgresql"];
|
|
106660
106872
|
ALLOWED_FORMATS21 = ["text", "json"];
|
|
106661
106873
|
ALLOWED_REDACT = ["none", "literals"];
|
|
@@ -106666,8 +106878,8 @@ var init_proxy = __esm(() => {
|
|
|
106666
106878
|
await runProxy(engine, options, command);
|
|
106667
106879
|
});
|
|
106668
106880
|
}
|
|
106669
|
-
ANALYZE_FORMATS = ["json", "text"];
|
|
106670
|
-
proxyCommand.command("analyze").description("Analyze a proxy event log offline (no DB connection)").option("--events <path>", "Event JSONL path", join38(".dbcli", "proxy", "events.jsonl")).option("--format <format>", "Output format: json | text", "json").option("--top <number>", "Rows shown in text + suggestedCommands depth", "20").option("--slow-ms <number>", "Slow-query threshold (ms) for slowCount", "1000").option("--n-plus-one <number>", "Min repeats per (session,fingerprint) to flag N+1", "10").option("--no-include-rotated", "Do not merge the rotated <events>.1 segment").action(async (options) => {
|
|
106881
|
+
ANALYZE_FORMATS = ["json", "text", "markdown"];
|
|
106882
|
+
proxyCommand.command("analyze").description("Analyze a proxy event log offline (no DB connection)").option("--events <path>", "Event JSONL path", join38(".dbcli", "proxy", "events.jsonl")).option("--format <format>", "Output format: json | text | markdown (QueryLens)", "json").option("--top <number>", "Rows shown in text + suggestedCommands depth", "20").option("--slow-ms <number>", "Slow-query threshold (ms) for slowCount", "1000").option("--n-plus-one <number>", "Min repeats per (session,fingerprint) to flag N+1", "10").option("--no-include-rotated", "Do not merge the rotated <events>.1 segment").action(async (options) => {
|
|
106671
106883
|
await runAnalyze(options);
|
|
106672
106884
|
});
|
|
106673
106885
|
addCommonOptions(proxyCommand).action(async (options, command) => {
|
package/dist/core.mjs
CHANGED
|
@@ -23219,17 +23219,37 @@ function projectRows(rows, selection) {
|
|
|
23219
23219
|
const columnNames = collectColumnNames(projectedRows);
|
|
23220
23220
|
return { rows: normalizeRows(projectedRows, columnNames), columnNames };
|
|
23221
23221
|
}
|
|
23222
|
-
function hasFieldPath(row,
|
|
23223
|
-
return readPath(row,
|
|
23222
|
+
function hasFieldPath(row, segments) {
|
|
23223
|
+
return readPath(row, segments).found;
|
|
23224
23224
|
}
|
|
23225
23225
|
function omitFieldPaths(rows, paths) {
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23226
|
+
const topLevel = new Set(paths);
|
|
23227
|
+
const dotted = paths.filter((path) => path.includes(".")).map((path) => ({ head: path.slice(0, path.indexOf(".")), segments: path.split(".") }));
|
|
23228
|
+
const maskRecord = (row) => {
|
|
23229
|
+
let projected = cloneRecord(row, topLevel);
|
|
23230
|
+
for (const { head, segments } of dotted) {
|
|
23231
|
+
const value = row[head];
|
|
23232
|
+
if (value !== null && typeof value === "object")
|
|
23233
|
+
projected = omitPath(projected, segments);
|
|
23234
|
+
}
|
|
23230
23235
|
return projected;
|
|
23236
|
+
};
|
|
23237
|
+
return rows.map((row) => {
|
|
23238
|
+
if (row === null || typeof row !== "object")
|
|
23239
|
+
return {};
|
|
23240
|
+
if (Array.isArray(row))
|
|
23241
|
+
return maskArrayRow(row, maskRecord);
|
|
23242
|
+
return maskRecord(row);
|
|
23231
23243
|
});
|
|
23232
23244
|
}
|
|
23245
|
+
function maskArrayRow(row, maskRecord) {
|
|
23246
|
+
const out = {};
|
|
23247
|
+
row.forEach((item, index) => {
|
|
23248
|
+
const masked = Array.isArray(item) ? maskArrayRow(item, maskRecord) : item !== null && typeof item === "object" ? maskRecord(item) : item;
|
|
23249
|
+
defineData(out, String(index), masked);
|
|
23250
|
+
});
|
|
23251
|
+
return out;
|
|
23252
|
+
}
|
|
23233
23253
|
function readPath(value, segments) {
|
|
23234
23254
|
if (segments.length === 0)
|
|
23235
23255
|
return { found: true, value };
|
|
@@ -23273,10 +23293,10 @@ function omitPath(value, segments) {
|
|
|
23273
23293
|
}
|
|
23274
23294
|
return out;
|
|
23275
23295
|
}
|
|
23276
|
-
function cloneRecord(value,
|
|
23296
|
+
function cloneRecord(value, omittedKeys) {
|
|
23277
23297
|
const out = {};
|
|
23278
23298
|
for (const [key, child] of Object.entries(value)) {
|
|
23279
|
-
if (key
|
|
23299
|
+
if (!omittedKeys.has(key))
|
|
23280
23300
|
defineData(out, key, child);
|
|
23281
23301
|
}
|
|
23282
23302
|
return out;
|
|
@@ -24116,6 +24136,16 @@ function matchesIndexGlob(pattern, name) {
|
|
|
24116
24136
|
}
|
|
24117
24137
|
|
|
24118
24138
|
// src/core/blacklist-validator.ts
|
|
24139
|
+
function flattenArrayRow(row) {
|
|
24140
|
+
const records = [];
|
|
24141
|
+
for (const item of row) {
|
|
24142
|
+
if (Array.isArray(item))
|
|
24143
|
+
records.push(...flattenArrayRow(item));
|
|
24144
|
+
else if (item !== null && typeof item === "object")
|
|
24145
|
+
records.push(item);
|
|
24146
|
+
}
|
|
24147
|
+
return records;
|
|
24148
|
+
}
|
|
24119
24149
|
function dedupe2(values) {
|
|
24120
24150
|
const seen = new Set;
|
|
24121
24151
|
const result = [];
|
|
@@ -24216,7 +24246,56 @@ class BlacklistValidator {
|
|
|
24216
24246
|
if (blacklistedColumns.length === 0) {
|
|
24217
24247
|
return { filteredRows: rows, omittedColumns: [] };
|
|
24218
24248
|
}
|
|
24219
|
-
const
|
|
24249
|
+
const probeNested = blacklistedColumns.some((path3) => path3.includes("."));
|
|
24250
|
+
const presentColumns = new Set(columnList);
|
|
24251
|
+
const nestedHeads = new Set;
|
|
24252
|
+
const collect = (record) => {
|
|
24253
|
+
for (const key of Object.getOwnPropertyNames(record)) {
|
|
24254
|
+
presentColumns.add(key);
|
|
24255
|
+
if (!probeNested)
|
|
24256
|
+
continue;
|
|
24257
|
+
const value = record[key];
|
|
24258
|
+
if (value !== null && typeof value === "object")
|
|
24259
|
+
nestedHeads.add(key);
|
|
24260
|
+
}
|
|
24261
|
+
};
|
|
24262
|
+
for (const row of rows) {
|
|
24263
|
+
if (row === null || typeof row !== "object")
|
|
24264
|
+
continue;
|
|
24265
|
+
if (Array.isArray(row)) {
|
|
24266
|
+
for (const item of flattenArrayRow(row))
|
|
24267
|
+
collect(item);
|
|
24268
|
+
continue;
|
|
24269
|
+
}
|
|
24270
|
+
collect(row);
|
|
24271
|
+
}
|
|
24272
|
+
const protectedPaths = new Set(blacklistedColumns);
|
|
24273
|
+
const omitted = new Set;
|
|
24274
|
+
for (const path3 of blacklistedColumns) {
|
|
24275
|
+
if (presentColumns.has(path3)) {
|
|
24276
|
+
omitted.add(path3);
|
|
24277
|
+
continue;
|
|
24278
|
+
}
|
|
24279
|
+
const dot = path3.indexOf(".");
|
|
24280
|
+
if (dot < 0)
|
|
24281
|
+
continue;
|
|
24282
|
+
if (!nestedHeads.has(path3.slice(0, dot)))
|
|
24283
|
+
continue;
|
|
24284
|
+
const segments = path3.split(".");
|
|
24285
|
+
if (rows.some((row) => hasFieldPath(row, segments)))
|
|
24286
|
+
omitted.add(path3);
|
|
24287
|
+
}
|
|
24288
|
+
for (const column of presentColumns) {
|
|
24289
|
+
let dot = column.indexOf(".");
|
|
24290
|
+
while (dot >= 0) {
|
|
24291
|
+
if (dot > 0 && protectedPaths.has(column.slice(0, dot))) {
|
|
24292
|
+
omitted.add(column);
|
|
24293
|
+
break;
|
|
24294
|
+
}
|
|
24295
|
+
dot = column.indexOf(".", dot + 1);
|
|
24296
|
+
}
|
|
24297
|
+
}
|
|
24298
|
+
const omittedColumns = Array.from(omitted);
|
|
24220
24299
|
if (omittedColumns.length === 0) {
|
|
24221
24300
|
return { filteredRows: rows, omittedColumns: [] };
|
|
24222
24301
|
}
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carllee1983/dbcli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "Database CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -81,8 +81,9 @@
|
|
|
81
81
|
"typecheck": "tsc --noEmit --pretty false",
|
|
82
82
|
"test:perf": "bun test ./tests/perf/*.bench.ts",
|
|
83
83
|
"lint": "eslint src tests scripts --ext .ts --max-warnings=0",
|
|
84
|
-
"
|
|
85
|
-
"format": "prettier --write
|
|
84
|
+
"format:check": "prettier --check .",
|
|
85
|
+
"format": "prettier --write .",
|
|
86
|
+
"lint:fix": "eslint src tests scripts --ext .ts --fix --max-warnings=0"
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
89
|
"cli-table3": "^0.6.5",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbcli-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "Database CLI skill and command reference for AI agents",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carl Lee",
|
|
@@ -26,11 +26,7 @@
|
|
|
26
26
|
"longDescription": "Installs the dbcli agent skill and reference guide so Codex can inspect schemas, query databases, respect blacklist boundaries, and recover from database errors through the dbcli command workflow.",
|
|
27
27
|
"developerName": "Carl Lee",
|
|
28
28
|
"category": "Productivity",
|
|
29
|
-
"capabilities": [
|
|
30
|
-
"Database",
|
|
31
|
-
"Local CLI",
|
|
32
|
-
"Agent Skill"
|
|
33
|
-
],
|
|
29
|
+
"capabilities": ["Database", "Local CLI", "Agent Skill"],
|
|
34
30
|
"defaultPrompt": [
|
|
35
31
|
"Inspect my database with dbcli.",
|
|
36
32
|
"Show schema safely with dbcli.",
|
|
@@ -363,7 +363,7 @@ Full flags and edge cases: see [reference.md](reference.md) `init` section.
|
|
|
363
363
|
| `assert` | query-only+ | **(v1.25)** SQL only. Verify an invariant; exit 1 on failure unless `--no-fail`. `--expect "rows>0\|value==X\|col:c not null\|unique\|between a and b\|>= n"`, `--vs <query> --compare rows\|value` (reconcile), `--against <snapshot> --tolerance <pct>`. |
|
|
364
364
|
| `verification` | n/a | Inspect and manage local verification artifacts. `list` / `show <id-or-path>` / `summary` are read-only; `prune` is dry-run by default and deletes only with `--execute --force`. Reads `<cwd>/.dbcli/verification/`; no DB connection, no audit writes. |
|
|
365
365
|
| `backfill artifact` | n/a | Build a bounded, reviewable source-to-SQL backfill artifact from JSON. Includes source/target identity, blacklist/schema preflight, read-back verification, and rollback hints; dry-run only and never executes writes. |
|
|
366
|
-
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events
|
|
366
|
+
| `proxy` | n/a | **(v1.26)** MySQL/MariaDB/PostgreSQL only. Local-dev observability proxy — relays app traffic to the real DB and appends query/latency/byte/error events to `.dbcli/proxy/events.jsonl`. Subcommands: `mysql` \| `mariadb` \| `postgresql`. `--listen`, `--target`, `--events` (default `.dbcli/proxy/events.jsonl`), `--slow-ms` (default `1000`), `--redact none\|literals` (default `none`). Observe-only. **(v1.27)** `proxy analyze` aggregates the event log offline into a JSON/text report (summary, byFingerprint, slowest, errors, hotTables, N+1) — `--format`, `--top`, `--slow-ms`, `--n-plus-one`; errors out if no events exist yet. **(v1.50)** `proxy analyze --format markdown` produces the QueryLens report and redacts SQL/error literals in-memory before analysis; use `proxy <engine> --redact literals` to protect the log itself. Actionable blocks carry `suggestedCommands` + `hints` so an agent can act: SELECT hotspots/N+1 → `explain` / `guide missing-index-for`, errors → `schema <table>` (verify names, never guess), N+1 → batch (JOIN / `IN (...)`). After analyzing, run each finding's `suggestedCommands`, read its `hints`, then propose the fix. |
|
|
367
367
|
| `status` | query-only+ | Safe JSON/text summary (no credentials). |
|
|
368
368
|
| `inspect` | query-only+ | Read-only context snapshot (connection, permission, blacklist, objects, snippets, context-aware `suggestedCommands`, and **(v1.23)** human-readable `hints`). `--for-agent` / `--brief` / `--no-connect` / `--require-schema-cache`. Supports `--recovery`. |
|
|
369
369
|
| `report` | query-only+ | Diagnostic report built from `@diag/*` snippets. `--section <health\|capacity\|perf>` (comma-separated to combine), `--brief`, `--for-agent`, `--no-connect`. |
|
|
@@ -1180,6 +1180,7 @@ dbcli --use prod proxy postgresql # infer target from na
|
|
|
1180
1180
|
|
|
1181
1181
|
dbcli proxy analyze # analyze .dbcli/proxy/events.jsonl (JSON)
|
|
1182
1182
|
dbcli proxy analyze --format text --top 10 # human-readable top-10 view
|
|
1183
|
+
dbcli proxy analyze --format markdown # QueryLens shareable, redacted Markdown report
|
|
1183
1184
|
dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
|
|
1184
1185
|
```
|
|
1185
1186
|
|
|
@@ -1200,7 +1201,16 @@ dbcli proxy analyze --slow-ms 200 --n-plus-one 5 # custom thresholds
|
|
|
1200
1201
|
|
|
1201
1202
|
**Log rotation:** all writes are serialized through one in-process chain (concurrent sessions never interleave partial lines). The event log auto-rotates to keep one rolling segment — when the next line would reach ~50 MiB or 200,000 entries, the current file is renamed to `<events>.1` (overwriting any prior segment) and a fresh file starts. Worst-case on-disk footprint is ~2× the byte cap.
|
|
1202
1203
|
|
|
1203
|
-
**`proxy analyze`** — offline aggregation of the event log (no DB). Flags: `--events <path>` (default `.dbcli/proxy/events.jsonl`), `--format json|text` (default `json`), `--top <n>` (default 20; text rows + suggestedCommands depth), `--slow-ms <ms>` (default 1000; recomputes slowCount), `--n-plus-one <n>` (default 10), `--no-include-rotated`. JSON report blocks: `summary`, `byFingerprint` (sorted by total time; SELECT entries in the top-N carry `suggestedCommands` for `explain` / `guide missing-index-for`), `slowest`, `errors`, `hotTables`, `repetition` (N+1 suspects). Reads the current log plus the rotated `.1` segment by default.
|
|
1204
|
+
**`proxy analyze`** — offline aggregation of the event log (no DB). Flags: `--events <path>` (default `.dbcli/proxy/events.jsonl`), `--format json|text|markdown` (default `json`), `--top <n>` (default 20; text rows + suggestedCommands depth), `--slow-ms <ms>` (default 1000; recomputes slowCount), `--n-plus-one <n>` (default 10), `--no-include-rotated`. JSON report blocks: `summary`, `byFingerprint` (sorted by total time; SELECT entries in the top-N carry `suggestedCommands` for `explain` / `guide missing-index-for`), `slowest`, `errors`, `hotTables`, `repetition` (N+1 suspects). Reads the current log plus the rotated `.1` segment by default. `markdown` is the QueryLens report: it redacts SQL and error-message literals in an in-memory copy before analysis, while leaving the source log untouched.
|
|
1205
|
+
|
|
1206
|
+
Every actionable block carries machine-readable next steps so an agent can move from "what is wrong" to "what to run":
|
|
1207
|
+
- `byFingerprint[]` — top-N SELECT entries get `suggestedCommands`: `dbcli explain "<sql>"` and `dbcli guide missing-index-for "<sql>"`.
|
|
1208
|
+
- `errors[]` — carries `tables`; emits `suggestedCommands` of `dbcli schema <table>` (capped at the first 3 tables) plus a `hints` note to verify table/column names before fixing (never guess column names). No tables known (e.g. a syntax error) → no `suggestedCommands`, but the hint still appears.
|
|
1209
|
+
- `repetition[]` — carries `statement` and a runnable `exampleSql` (the slowest occurrence). SELECT N+1 groups get `explain` / `guide missing-index-for` `suggestedCommands`; every group carries a `hints` note suggesting batching (JOIN / `IN (...)`) or caching.
|
|
1210
|
+
|
|
1211
|
+
`suggestedCommands` are emitted as strings only — `proxy analyze` never executes them. When the proxy ran with `--redact literals`, `exampleSql` (and therefore the suggested commands) contains `?` placeholders; fill in real values before running them.
|
|
1212
|
+
|
|
1213
|
+
**Acting on the report (agent loop):** after `proxy analyze`, for each block read `hints` for the diagnosis, run the entry's `suggestedCommands` to gather schema/plan/index evidence, then propose a concrete fix — add an index (`guide missing-index-for`), rewrite a slow SELECT (`explain`), batch an N+1 (`repetition`), or correct a column/table name (`errors` → `schema`). The text format mirrors this with aggregated `SUGGESTED COMMANDS` and `HINTS` sections; JSON keeps the suggestions attached per-finding.
|
|
1204
1214
|
|
|
1205
1215
|
**Engines:** MySQL / MariaDB / PostgreSQL
|
|
1206
1216
|
**Permission:** n/a (acts as a TCP relay; does not use dbcli's SQL permission model)
|