@carllee1983/dbcli 1.6.0 → 1.7.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/dist/cli.mjs CHANGED
@@ -7528,11 +7528,11 @@ var init_schema_index = __esm(() => {
7528
7528
  // src/core/schema-loader.ts
7529
7529
  var exports_schema_loader = {};
7530
7530
  __export(exports_schema_loader, {
7531
- SchemaLayeredLoader: () => SchemaLayeredLoader2
7531
+ SchemaLayeredLoader: () => SchemaLayeredLoader
7532
7532
  });
7533
7533
  import { join as join6 } from "path";
7534
7534
 
7535
- class SchemaLayeredLoader2 {
7535
+ class SchemaLayeredLoader {
7536
7536
  dbcliPath;
7537
7537
  connectionName;
7538
7538
  options;
@@ -77096,6 +77096,49 @@ var init_size_category = __esm(() => {
77096
77096
  ];
77097
77097
  });
77098
77098
 
77099
+ // src/utils/levenshtein-distance.ts
77100
+ function levenshteinDistance(a, b) {
77101
+ const lenA = a.length;
77102
+ const lenB = b.length;
77103
+ const dp = [];
77104
+ for (let i = 0;i <= lenB; i++) {
77105
+ const row = [];
77106
+ for (let j = 0;j <= lenA; j++) {
77107
+ row[j] = 0;
77108
+ }
77109
+ dp[i] = row;
77110
+ }
77111
+ for (let i = 0;i <= lenB; i++) {
77112
+ const row = dp[i];
77113
+ if (row !== undefined) {
77114
+ row[0] = i;
77115
+ }
77116
+ }
77117
+ for (let j = 0;j <= lenA; j++) {
77118
+ const firstRow = dp[0];
77119
+ if (firstRow !== undefined) {
77120
+ firstRow[j] = j;
77121
+ }
77122
+ }
77123
+ for (let i = 1;i <= lenB; i++) {
77124
+ const currentRow = dp[i];
77125
+ const prevRow = dp[i - 1];
77126
+ if (!currentRow || !prevRow)
77127
+ continue;
77128
+ for (let j = 1;j <= lenA; j++) {
77129
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
77130
+ currentRow[j] = prevRow[j - 1] ?? 0;
77131
+ } else {
77132
+ const sub = (prevRow[j - 1] ?? 0) + 1;
77133
+ const ins = (currentRow[j - 1] ?? 0) + 1;
77134
+ const del = (prevRow[j] ?? 0) + 1;
77135
+ currentRow[j] = Math.min(sub, ins, del);
77136
+ }
77137
+ }
77138
+ }
77139
+ return dp[lenB]?.[lenA] ?? 0;
77140
+ }
77141
+
77099
77142
  // src/commands/query-size-guard.ts
77100
77143
  var exports_query_size_guard = {};
77101
77144
  __export(exports_query_size_guard, {
@@ -77128,6 +77171,819 @@ var init_query_size_guard = __esm(() => {
77128
77171
  init_size_category();
77129
77172
  });
77130
77173
 
77174
+ // src/core/saved-queries/types.ts
77175
+ var SavedQueryError;
77176
+ var init_types2 = __esm(() => {
77177
+ SavedQueryError = class SavedQueryError extends Error {
77178
+ code;
77179
+ file;
77180
+ constructor(message, code, file) {
77181
+ super(message);
77182
+ this.code = code;
77183
+ this.file = file;
77184
+ this.name = "SavedQueryError";
77185
+ Object.setPrototypeOf(this, SavedQueryError.prototype);
77186
+ }
77187
+ };
77188
+ });
77189
+
77190
+ // src/core/saved-queries/yaml-mini.ts
77191
+ function parseYamlMini(text2) {
77192
+ const lines2 = text2.split(`
77193
+ `).filter((l) => !/^\s*(#.*)?$/.test(l));
77194
+ const root = {};
77195
+ const stack = [{ indent: -1, node: root }];
77196
+ for (const raw of lines2) {
77197
+ if (raw.includes("\t")) {
77198
+ throw new Error(`YAML mini: tab indentation not supported: "${raw}"`);
77199
+ }
77200
+ const indent = raw.match(/^( *)/)[1].length;
77201
+ const line = raw.slice(indent);
77202
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
77203
+ stack.pop();
77204
+ }
77205
+ const parent = stack[stack.length - 1].node;
77206
+ const colon = line.indexOf(":");
77207
+ if (colon === -1) {
77208
+ throw new Error(`YAML mini: expected "key:" at "${raw}"`);
77209
+ }
77210
+ const key2 = line.slice(0, colon).trim();
77211
+ const rest = line.slice(colon + 1).trim();
77212
+ if (/^[&*]\w/.test(rest)) {
77213
+ throw new Error(`YAML mini: anchor/reference unsupported: "${raw}"`);
77214
+ }
77215
+ if (rest === "") {
77216
+ const child = {};
77217
+ parent[key2] = child;
77218
+ stack.push({ indent, node: child });
77219
+ } else {
77220
+ parent[key2] = parseScalarOrInlineList(rest);
77221
+ }
77222
+ }
77223
+ return root;
77224
+ }
77225
+ function parseScalarOrInlineList(s) {
77226
+ if (s.startsWith("[") && s.endsWith("]")) {
77227
+ const inner = s.slice(1, -1).trim();
77228
+ if (inner === "")
77229
+ return [];
77230
+ return inner.split(",").map((p) => parseScalar(p.trim()));
77231
+ }
77232
+ if (s.startsWith("{") && s.endsWith("}")) {
77233
+ const inner = s.slice(1, -1).trim();
77234
+ if (inner === "")
77235
+ return {};
77236
+ throw new Error(`YAML mini: inline map entries unsupported, use block form: "${s}"`);
77237
+ }
77238
+ return parseScalar(s);
77239
+ }
77240
+ function parseScalar(s) {
77241
+ if (s === "true")
77242
+ return true;
77243
+ if (s === "false")
77244
+ return false;
77245
+ if (s === "null" || s === "~" || s === "")
77246
+ return null;
77247
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
77248
+ return s.slice(1, -1);
77249
+ }
77250
+ if (/^-?\d+$/.test(s))
77251
+ return parseInt(s, 10);
77252
+ if (/^-?\d+\.\d+$/.test(s))
77253
+ return parseFloat(s);
77254
+ return s;
77255
+ }
77256
+
77257
+ // src/core/saved-queries/parser.ts
77258
+ function parseSavedQuery(input) {
77259
+ if (Buffer.byteLength(input.text, "utf8") > MAX_BYTES) {
77260
+ throw new SavedQueryError(`Snippet '${input.key}' exceeds 64 KB size limit`, "FILE_TOO_LARGE", input.file);
77261
+ }
77262
+ const { frontmatter, body } = splitFrontmatter(input.text);
77263
+ const fm = parseFrontmatter(frontmatter, input);
77264
+ validateBody(body, input);
77265
+ const meta = fm.meta;
77266
+ meta.key = input.key;
77267
+ if (!meta.name)
77268
+ meta.name = input.key;
77269
+ return {
77270
+ query: { meta, sqlBody: body, file: input.file, source: input.source },
77271
+ warnings: fm.warnings
77272
+ };
77273
+ }
77274
+ function splitFrontmatter(text2) {
77275
+ const lines2 = text2.split(`
77276
+ `);
77277
+ if (lines2[0]?.trim() !== "-- ---")
77278
+ return { frontmatter: "", body: text2 };
77279
+ const end = lines2.findIndex((l, i) => i > 0 && l.trim() === "-- ---");
77280
+ if (end === -1)
77281
+ return { frontmatter: "", body: text2 };
77282
+ const fmLines = lines2.slice(1, end).map((l) => l.replace(/^--\s?/, ""));
77283
+ const bodyLines = lines2.slice(end + 1);
77284
+ return { frontmatter: fmLines.join(`
77285
+ `), body: bodyLines.join(`
77286
+ `) };
77287
+ }
77288
+ function parseFrontmatter(yaml, input) {
77289
+ if (!yaml.trim()) {
77290
+ return {
77291
+ meta: { name: "", key: input.key, params: [], tags: [] },
77292
+ warnings: ["Snippet has no engine declaration; assuming any engine"]
77293
+ };
77294
+ }
77295
+ let rawParsed;
77296
+ try {
77297
+ rawParsed = parseYamlMini(yaml);
77298
+ } catch (e) {
77299
+ throw new SavedQueryError(`Invalid frontmatter in '${input.key}': ${e.message}`, "PARSE_ERROR", input.file);
77300
+ }
77301
+ const warnings = [];
77302
+ const raw = rawParsed ?? {};
77303
+ const engine = normaliseEngine(raw.engine, warnings);
77304
+ const params = normaliseParams(raw.params, input);
77305
+ const tags = Array.isArray(raw.tags) ? raw.tags.map(String) : [];
77306
+ return {
77307
+ meta: {
77308
+ name: typeof raw.name === "string" ? raw.name : "",
77309
+ key: input.key,
77310
+ description: typeof raw.description === "string" ? raw.description : undefined,
77311
+ engine,
77312
+ params,
77313
+ tags
77314
+ },
77315
+ warnings
77316
+ };
77317
+ }
77318
+ function normaliseEngine(value, warnings) {
77319
+ if (value === undefined || value === null || value === "") {
77320
+ warnings.push("Snippet has no engine declaration; assuming any engine");
77321
+ return;
77322
+ }
77323
+ const list = Array.isArray(value) ? value : [value];
77324
+ const cleaned = [];
77325
+ for (const v of list) {
77326
+ const s = String(v).toLowerCase();
77327
+ if (!VALID_ENGINES.includes(s)) {
77328
+ throw new SavedQueryError(`Unknown engine '${s}' (allowed: postgres, mysql)`, "PARSE_ERROR");
77329
+ }
77330
+ cleaned.push(s);
77331
+ }
77332
+ return cleaned;
77333
+ }
77334
+ function normaliseParams(value, input) {
77335
+ if (value === undefined || value === null)
77336
+ return [];
77337
+ if (typeof value !== "object" || Array.isArray(value)) {
77338
+ throw new SavedQueryError(`'params' must be a map`, "PARSE_ERROR", input.file);
77339
+ }
77340
+ return Object.entries(value).map(([name, spec]) => {
77341
+ const type = String(spec?.type ?? "string");
77342
+ if (!VALID_TYPES.includes(type)) {
77343
+ throw new SavedQueryError(`Param '${name}': invalid type '${type}'`, "PARSE_ERROR", input.file);
77344
+ }
77345
+ const hasDefault = spec && Object.prototype.hasOwnProperty.call(spec, "default");
77346
+ return {
77347
+ name,
77348
+ type,
77349
+ required: spec?.required === true ? true : !hasDefault,
77350
+ default: hasDefault ? spec.default : undefined,
77351
+ description: typeof spec?.description === "string" ? spec.description : undefined,
77352
+ enum: Array.isArray(spec?.enum) ? spec.enum : undefined
77353
+ };
77354
+ });
77355
+ }
77356
+ function validateBody(body, input) {
77357
+ const stripped = stripCommentsAndStrings2(body);
77358
+ const trimmed = stripped.trim();
77359
+ if (!trimmed) {
77360
+ throw new SavedQueryError(`Snippet '${input.key}' has empty SQL body`, "PARSE_ERROR", input.file);
77361
+ }
77362
+ const firstKeyword = trimmed.match(/^[A-Za-z]+/)?.[0]?.toUpperCase() ?? "";
77363
+ if (firstKeyword !== "SELECT" && firstKeyword !== "WITH") {
77364
+ throw new SavedQueryError(`Snippet '${input.key}' must start with SELECT or WITH (got '${firstKeyword || "<empty>"}')`, "NOT_SELECT", input.file);
77365
+ }
77366
+ const semi = trimmed.indexOf(";");
77367
+ if (semi !== -1) {
77368
+ const tail = trimmed.slice(semi + 1);
77369
+ if (/[A-Za-z0-9_]/.test(tail)) {
77370
+ throw new SavedQueryError(`Snippet '${input.key}' contains multiple statements; only one SELECT is allowed`, "MULTI_STATEMENT", input.file);
77371
+ }
77372
+ }
77373
+ if (/\$\{[^}]*\}|\{\{[^}]*\}\}/.test(body)) {
77374
+ throw new SavedQueryError(`Snippet '${input.key}' uses template syntax; use :name bind parameters instead`, "TEMPLATE_SYNTAX", input.file);
77375
+ }
77376
+ }
77377
+ function stripCommentsAndStrings2(sql) {
77378
+ let out = "";
77379
+ let i = 0;
77380
+ while (i < sql.length) {
77381
+ const c = sql[i];
77382
+ const next = sql[i + 1];
77383
+ if (c === "-" && next === "-") {
77384
+ while (i < sql.length && sql[i] !== `
77385
+ `) {
77386
+ out += " ";
77387
+ i++;
77388
+ }
77389
+ continue;
77390
+ }
77391
+ if (c === "/" && next === "*") {
77392
+ out += " ";
77393
+ i += 2;
77394
+ while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
77395
+ out += sql[i] === `
77396
+ ` ? `
77397
+ ` : " ";
77398
+ i++;
77399
+ }
77400
+ if (i < sql.length) {
77401
+ out += " ";
77402
+ i += 2;
77403
+ }
77404
+ continue;
77405
+ }
77406
+ if (c === "'" || c === '"') {
77407
+ const quote = c;
77408
+ out += " ";
77409
+ i++;
77410
+ while (i < sql.length) {
77411
+ if (sql[i] === "\\" && i + 1 < sql.length) {
77412
+ out += " ";
77413
+ i += 2;
77414
+ continue;
77415
+ }
77416
+ if (sql[i] === quote) {
77417
+ out += " ";
77418
+ i++;
77419
+ break;
77420
+ }
77421
+ out += sql[i] === `
77422
+ ` ? `
77423
+ ` : " ";
77424
+ i++;
77425
+ }
77426
+ continue;
77427
+ }
77428
+ if (c === "$" && next === "$") {
77429
+ out += " ";
77430
+ i += 2;
77431
+ while (i < sql.length && !(sql[i] === "$" && sql[i + 1] === "$")) {
77432
+ out += sql[i] === `
77433
+ ` ? `
77434
+ ` : " ";
77435
+ i++;
77436
+ }
77437
+ if (i < sql.length) {
77438
+ out += " ";
77439
+ i += 2;
77440
+ }
77441
+ continue;
77442
+ }
77443
+ out += c;
77444
+ i++;
77445
+ }
77446
+ return out;
77447
+ }
77448
+ var MAX_BYTES, VALID_TYPES, VALID_ENGINES;
77449
+ var init_parser = __esm(() => {
77450
+ init_types2();
77451
+ MAX_BYTES = 64 * 1024;
77452
+ VALID_TYPES = ["int", "string", "float", "bool", "date", "datetime"];
77453
+ VALID_ENGINES = ["postgres", "mysql"];
77454
+ });
77455
+
77456
+ // src/core/saved-queries/loader.ts
77457
+ import { readdir } from "fs/promises";
77458
+ import { join as join10, relative, sep } from "path";
77459
+ async function loadSnippets(opts) {
77460
+ const builtin = await walkAndParse(opts.builtinDir, "builtin");
77461
+ const shared = await walkAndParse(opts.sharedDir, "shared");
77462
+ const local = await walkAndParse(opts.localDir, "local");
77463
+ const merged = new Map;
77464
+ pushTier(merged, builtin);
77465
+ pushTier(merged, shared);
77466
+ pushTier(merged, local);
77467
+ return merged;
77468
+ }
77469
+ function pushTier(out, tier) {
77470
+ for (const q of tier) {
77471
+ const list = out.get(q.meta.key) ?? [];
77472
+ const keepers = list.filter((existing) => !sameEngineSet(existing.query, q));
77473
+ const replacedSomething = keepers.length < list.length;
77474
+ keepers.push({ query: q, hasLocalOverride: q.source === "local" && replacedSomething });
77475
+ out.set(q.meta.key, keepers);
77476
+ }
77477
+ }
77478
+ function sameEngineSet(a, b) {
77479
+ const ae = (a.meta.engine ?? []).slice().sort().join(",");
77480
+ const be = (b.meta.engine ?? []).slice().sort().join(",");
77481
+ return ae === be;
77482
+ }
77483
+ async function walkAndParse(root, source) {
77484
+ const out = [];
77485
+ let entries;
77486
+ try {
77487
+ entries = await collectFiles(root);
77488
+ } catch {
77489
+ return out;
77490
+ }
77491
+ for (const file of entries) {
77492
+ if (!file.endsWith(".sql"))
77493
+ continue;
77494
+ const rel = relative(root, file).replace(new RegExp(`\\${sep}`, "g"), "/");
77495
+ const { logicalKey, suffixEngine } = parseFilename(rel);
77496
+ const text2 = await Bun.file(file).text();
77497
+ const { query } = parseSavedQuery({ key: logicalKey, file, source, text: text2 });
77498
+ enforceSuffixEngineConsistency(query, suffixEngine);
77499
+ out.push(query);
77500
+ }
77501
+ return out;
77502
+ }
77503
+ function parseFilename(rel) {
77504
+ const noExt = rel.slice(0, -".sql".length);
77505
+ for (const eng of ENGINE_SUFFIXES) {
77506
+ if (noExt.endsWith("." + eng)) {
77507
+ return { logicalKey: "@" + noExt.slice(0, -("." + eng).length), suffixEngine: eng };
77508
+ }
77509
+ }
77510
+ return { logicalKey: "@" + noExt, suffixEngine: null };
77511
+ }
77512
+ function enforceSuffixEngineConsistency(q, suffixEngine) {
77513
+ if (!suffixEngine)
77514
+ return;
77515
+ const declared = q.meta.engine ?? [];
77516
+ if (declared.length !== 1 || declared[0] !== suffixEngine) {
77517
+ q.meta.engine = [suffixEngine];
77518
+ }
77519
+ }
77520
+ async function collectFiles(root) {
77521
+ const out = [];
77522
+ async function walk(dir) {
77523
+ const entries = await readdir(dir, { withFileTypes: true });
77524
+ for (const e of entries) {
77525
+ const full = join10(dir, e.name);
77526
+ if (e.isDirectory())
77527
+ await walk(full);
77528
+ else
77529
+ out.push(full);
77530
+ }
77531
+ }
77532
+ await walk(root);
77533
+ return out;
77534
+ }
77535
+ var ENGINE_SUFFIXES;
77536
+ var init_loader = __esm(() => {
77537
+ init_parser();
77538
+ ENGINE_SUFFIXES = ["postgres", "mysql"];
77539
+ });
77540
+
77541
+ // src/core/saved-queries/binder.ts
77542
+ function mergeParamSources(cli, file) {
77543
+ return { ...file, ...cli };
77544
+ }
77545
+ function coerceParams(specs, raw) {
77546
+ const out = {};
77547
+ const missing = [];
77548
+ for (const spec of specs) {
77549
+ const provided = Object.prototype.hasOwnProperty.call(raw, spec.name);
77550
+ if (!provided) {
77551
+ if (spec.required) {
77552
+ missing.push(spec.name);
77553
+ continue;
77554
+ }
77555
+ out[spec.name] = spec.default ?? null;
77556
+ continue;
77557
+ }
77558
+ out[spec.name] = coerceValue(spec, raw[spec.name]);
77559
+ }
77560
+ if (missing.length > 0) {
77561
+ throw new SavedQueryError(`Missing required parameters: ${missing.join(", ")}`, "PARAM_MISSING");
77562
+ }
77563
+ return out;
77564
+ }
77565
+ function coerceValue(spec, value) {
77566
+ const s = value === null || value === undefined ? "" : String(value);
77567
+ let coerced;
77568
+ switch (spec.type) {
77569
+ case "int": {
77570
+ const n = parseInt(s, 10);
77571
+ if (Number.isNaN(n))
77572
+ throw paramErr(spec, s, "int");
77573
+ coerced = n;
77574
+ break;
77575
+ }
77576
+ case "float": {
77577
+ const n = parseFloat(s);
77578
+ if (Number.isNaN(n))
77579
+ throw paramErr(spec, s, "float");
77580
+ coerced = n;
77581
+ break;
77582
+ }
77583
+ case "bool": {
77584
+ const lc = s.toLowerCase();
77585
+ if (["true", "1", "yes"].includes(lc))
77586
+ coerced = true;
77587
+ else if (["false", "0", "no"].includes(lc))
77588
+ coerced = false;
77589
+ else
77590
+ throw paramErr(spec, s, "bool");
77591
+ break;
77592
+ }
77593
+ case "date": {
77594
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s))
77595
+ throw paramErr(spec, s, "date (YYYY-MM-DD)");
77596
+ coerced = s;
77597
+ break;
77598
+ }
77599
+ case "datetime": {
77600
+ if (Number.isNaN(Date.parse(s)))
77601
+ throw paramErr(spec, s, "datetime (ISO 8601)");
77602
+ coerced = s;
77603
+ break;
77604
+ }
77605
+ case "string":
77606
+ default:
77607
+ coerced = s;
77608
+ }
77609
+ if (spec.enum && !spec.enum.some((v) => String(v) === String(coerced))) {
77610
+ throw new SavedQueryError(`Param '${spec.name}': value '${coerced}' not in enum [${spec.enum.join(", ")}]`, "PARAM_INVALID");
77611
+ }
77612
+ return coerced;
77613
+ }
77614
+ function paramErr(spec, raw, expected) {
77615
+ return new SavedQueryError(`Param '${spec.name}': cannot coerce '${raw}' to ${expected}`, "PARAM_INVALID");
77616
+ }
77617
+ function rewriteToBind(sqlBody, params, engine) {
77618
+ const masked = stripCommentsAndStrings2(sqlBody);
77619
+ const order = [];
77620
+ const indexByName = new Map;
77621
+ let m;
77622
+ NAME_RE.lastIndex = 0;
77623
+ while ((m = NAME_RE.exec(masked)) !== null) {
77624
+ const name = m[1];
77625
+ if (!indexByName.has(name)) {
77626
+ indexByName.set(name, order.length + 1);
77627
+ order.push(name);
77628
+ }
77629
+ }
77630
+ let outSql = "";
77631
+ let cursor = 0;
77632
+ NAME_RE.lastIndex = 0;
77633
+ while ((m = NAME_RE.exec(masked)) !== null) {
77634
+ const name = m[1];
77635
+ const start = m.index;
77636
+ const end = start + m[0].length;
77637
+ outSql += sqlBody.slice(cursor, start);
77638
+ const idx = indexByName.get(name);
77639
+ outSql += engine === "postgres" ? `$${idx}` : "?";
77640
+ cursor = end;
77641
+ }
77642
+ outSql += sqlBody.slice(cursor);
77643
+ const undeclared = [];
77644
+ const values = order.map((n) => {
77645
+ if (!Object.prototype.hasOwnProperty.call(params, n)) {
77646
+ undeclared.push(n);
77647
+ return null;
77648
+ }
77649
+ return params[n];
77650
+ });
77651
+ return { sql: outSql, values, undeclared };
77652
+ }
77653
+ var NAME_RE;
77654
+ var init_binder = __esm(() => {
77655
+ init_types2();
77656
+ init_parser();
77657
+ NAME_RE = /(?<![\w:]):([a-zA-Z_][a-zA-Z0-9_]*)/g;
77658
+ });
77659
+
77660
+ // src/core/saved-queries/resolver.ts
77661
+ function resolveByName(map, name, engine) {
77662
+ const variants = map.get(name);
77663
+ if (!variants || variants.length === 0) {
77664
+ const suggestions = suggestSimilar([...map.keys()], name);
77665
+ const hint = suggestions.length > 0 ? `
77666
+ Did you mean: ${suggestions.join(", ")}?` : "";
77667
+ throw new SavedQueryError(`Snippet not found: ${name}${hint}`, "NOT_FOUND");
77668
+ }
77669
+ const matches = variants.filter((v) => engineMatches(v.query.meta.engine, engine));
77670
+ if (matches.length === 0) {
77671
+ const declared = variants.map((v) => v.query.meta.engine?.join(",") ?? "any").join(" | ");
77672
+ throw new SavedQueryError(`Snippet '${name}' does not support engine '${engine}' (declared: ${declared})`, "ENGINE_MISMATCH");
77673
+ }
77674
+ matches.sort((a, b) => SOURCE_RANK[b.query.source] - SOURCE_RANK[a.query.source]);
77675
+ return matches[0];
77676
+ }
77677
+ function engineMatches(declared, current) {
77678
+ if (!declared || declared.length === 0)
77679
+ return true;
77680
+ return declared.includes(current);
77681
+ }
77682
+ function suggestSimilar(all, name, limit = 5) {
77683
+ return all.map((k) => ({ k, d: levenshteinDistance(k, name) })).sort((a, b) => a.d - b.d).slice(0, limit).map((x) => x.k);
77684
+ }
77685
+ var SOURCE_RANK;
77686
+ var init_resolver = __esm(() => {
77687
+ init_types2();
77688
+ SOURCE_RANK = {
77689
+ builtin: 0,
77690
+ shared: 1,
77691
+ local: 2
77692
+ };
77693
+ });
77694
+
77695
+ // src/core/saved-queries/size-guard.ts
77696
+ function applySnippetGuard(sqlBody, opts) {
77697
+ const trimmed = sqlBody.trim().replace(/;\s*$/, "");
77698
+ if (opts.noLimit)
77699
+ return trimmed;
77700
+ const masked = stripCommentsAndStrings2(trimmed);
77701
+ const literal = matchOuterLiteralLimit(masked);
77702
+ if (literal !== null && literal < GUARD_LIMIT)
77703
+ return trimmed;
77704
+ return `SELECT * FROM (${trimmed}) AS _dbcli_guard LIMIT ${GUARD_LIMIT}`;
77705
+ }
77706
+ function matchOuterLiteralLimit(masked) {
77707
+ const m = masked.match(/\bLIMIT\s+(\d+)\s*$/i);
77708
+ return m ? parseInt(m[1], 10) : null;
77709
+ }
77710
+ var GUARD_LIMIT = 1000;
77711
+ var init_size_guard = __esm(() => {
77712
+ init_parser();
77713
+ });
77714
+
77715
+ // src/core/saved-queries/runner.ts
77716
+ function prepareExecution(snippet, opts, cliParams, fileParams) {
77717
+ const warnings = [];
77718
+ if (opts.engine === "mongodb") {
77719
+ throw new SavedQueryError(`Snippet '${snippet.query.meta.key}' targets SQL but current connection is MongoDB`, "ENGINE_MISMATCH", snippet.query.file);
77720
+ }
77721
+ const declared = snippet.query.meta.engine;
77722
+ if (!declared) {
77723
+ warnings.push(`Snippet '${snippet.query.meta.key}' has no engine declaration`);
77724
+ } else if (!declared.includes(opts.engine)) {
77725
+ throw new SavedQueryError(`Engine mismatch for snippet '${snippet.query.meta.key}'
77726
+ ` + ` Snippet requires: ${declared.join(", ")}
77727
+ ` + ` Connection is: ${opts.engine}`, "ENGINE_MISMATCH", snippet.query.file);
77728
+ }
77729
+ const merged = mergeParamSources(cliParams, fileParams);
77730
+ const typed = coerceParams(snippet.query.meta.params, merged);
77731
+ const rewritten = rewriteToBind(snippet.query.sqlBody, typed, opts.engine);
77732
+ if (rewritten.undeclared.length > 0) {
77733
+ warnings.push(`SQL references undeclared params: ${rewritten.undeclared.join(", ")}`);
77734
+ }
77735
+ const wrapped = applySnippetGuard(rewritten.sql, { noLimit: opts.noLimit });
77736
+ return {
77737
+ driver: { sql: wrapped, values: rewritten.values },
77738
+ rewrittenSql: rewritten.sql,
77739
+ warnings
77740
+ };
77741
+ }
77742
+ var init_runner = __esm(() => {
77743
+ init_binder();
77744
+ init_size_guard();
77745
+ init_types2();
77746
+ });
77747
+
77748
+ // src/core/saved-queries/snippet-paths.ts
77749
+ import { join as join11, resolve as resolve2 } from "path";
77750
+ function resolveBuiltinDir() {
77751
+ return resolve2(import.meta.dir, "..", "..", "..", "assets", "snippets");
77752
+ }
77753
+ function resolveSnippetDirs(workspaceRoot) {
77754
+ return {
77755
+ builtinDir: resolveBuiltinDir(),
77756
+ sharedDir: join11(workspaceRoot, ".dbcli-shared", "queries"),
77757
+ localDir: join11(workspaceRoot, ".dbcli", "queries")
77758
+ };
77759
+ }
77760
+ function snippetKeyToFile(workspaceRoot, key2, source) {
77761
+ const rel = key2.replace(/^@/, "") + ".sql";
77762
+ if (source === "builtin")
77763
+ return join11(resolveBuiltinDir(), rel);
77764
+ const dir = source === "shared" ? ".dbcli-shared/queries" : ".dbcli/queries";
77765
+ return join11(workspaceRoot, dir, rel);
77766
+ }
77767
+ var init_snippet_paths = () => {};
77768
+
77769
+ // src/core/saved-queries/engine-map.ts
77770
+ function mapSystemToEngine(system) {
77771
+ if (system === "postgresql")
77772
+ return "postgres";
77773
+ if (system === "mysql" || system === "mariadb")
77774
+ return "mysql";
77775
+ if (system === "mongodb")
77776
+ return "mongodb";
77777
+ throw new Error(`Unsupported connection system for snippets: ${system}`);
77778
+ }
77779
+
77780
+ // src/core/saved-queries/index.ts
77781
+ var init_saved_queries = __esm(() => {
77782
+ init_types2();
77783
+ init_loader();
77784
+ init_parser();
77785
+ init_binder();
77786
+ init_resolver();
77787
+ init_size_guard();
77788
+ init_runner();
77789
+ init_snippet_paths();
77790
+ });
77791
+
77792
+ // src/commands/queries-delete.ts
77793
+ var exports_queries_delete = {};
77794
+ __export(exports_queries_delete, {
77795
+ queriesDelete: () => queriesDelete
77796
+ });
77797
+ import { rm } from "fs/promises";
77798
+ async function queriesDelete(name, options = {}) {
77799
+ if (!name.startsWith("@")) {
77800
+ throw new Error(`Snippet name must start with '@' (got '${name}')`);
77801
+ }
77802
+ const cwd = options.cwd ?? process.cwd();
77803
+ const dirs = resolveSnippetDirs(cwd);
77804
+ const map = await loadSnippets(dirs);
77805
+ const variants = map.get(name);
77806
+ if (!variants || variants.length === 0) {
77807
+ throw new Error(`Snippet not found: ${name}`);
77808
+ }
77809
+ const localVariants = variants.filter((v) => v.query.source === "local");
77810
+ if (localVariants.length === 0) {
77811
+ throw new Error(`Refusing to delete: '${name}' has no local copy. Only local snippets can be deleted.`);
77812
+ }
77813
+ if (!options.force) {
77814
+ const ok = await esm_default4({
77815
+ message: `Delete ${localVariants.length} local file(s) for ${name}?`,
77816
+ default: false
77817
+ });
77818
+ if (!ok)
77819
+ return;
77820
+ }
77821
+ for (const v of localVariants) {
77822
+ await rm(v.query.file, { force: true });
77823
+ console.log(`deleted ${v.query.file}`);
77824
+ }
77825
+ }
77826
+ var init_queries_delete = __esm(() => {
77827
+ init_esm14();
77828
+ init_saved_queries();
77829
+ });
77830
+
77831
+ // src/commands/queries-rename.ts
77832
+ var exports_queries_rename = {};
77833
+ __export(exports_queries_rename, {
77834
+ queriesRename: () => queriesRename
77835
+ });
77836
+ import { rename, mkdir } from "fs/promises";
77837
+ import { dirname } from "path";
77838
+ async function queriesRename(oldName, newName, options = {}) {
77839
+ if (!oldName.startsWith("@") || !newName.startsWith("@")) {
77840
+ throw new Error(`Both names must start with '@'`);
77841
+ }
77842
+ const cwd = options.cwd ?? process.cwd();
77843
+ const dirs = resolveSnippetDirs(cwd);
77844
+ const map = await loadSnippets(dirs);
77845
+ const variants = map.get(oldName);
77846
+ if (!variants || variants.length === 0)
77847
+ throw new Error(`Snippet not found: ${oldName}`);
77848
+ const local = variants.filter((v) => v.query.source === "local");
77849
+ if (local.length === 0) {
77850
+ throw new Error(`Refusing to rename: '${oldName}' has no local copy.`);
77851
+ }
77852
+ if (map.get(newName)?.some((v) => v.query.source === "local")) {
77853
+ throw new Error(`Target already exists: ${newName}`);
77854
+ }
77855
+ for (const v of local) {
77856
+ const dst = snippetKeyToFile(cwd, newName, "local");
77857
+ const dstWithSuffix = preserveEngineSuffix(v.query.file, dst);
77858
+ await mkdir(dirname(dstWithSuffix), { recursive: true });
77859
+ await rename(v.query.file, dstWithSuffix);
77860
+ await rewriteFrontmatterName(dstWithSuffix, newName.slice(1));
77861
+ console.log(`renamed ${v.query.file} \u2192 ${dstWithSuffix}`);
77862
+ }
77863
+ }
77864
+ function preserveEngineSuffix(srcFile, dstFile) {
77865
+ const m = srcFile.match(/\.(postgres|mysql)\.sql$/);
77866
+ if (!m)
77867
+ return dstFile;
77868
+ return dstFile.replace(/\.sql$/, `.${m[1]}.sql`);
77869
+ }
77870
+ async function rewriteFrontmatterName(file, newName) {
77871
+ const text2 = await Bun.file(file).text();
77872
+ const updated = text2.replace(/^-- name:.*$/m, `-- name: ${newName}`);
77873
+ await Bun.write(file, updated);
77874
+ }
77875
+ var init_queries_rename = __esm(() => {
77876
+ init_saved_queries();
77877
+ });
77878
+
77879
+ // src/commands/queries-copy.ts
77880
+ var exports_queries_copy = {};
77881
+ __export(exports_queries_copy, {
77882
+ queriesCopy: () => queriesCopy
77883
+ });
77884
+ import { mkdir as mkdir2, copyFile } from "fs/promises";
77885
+ import { dirname as dirname2, basename as basename2 } from "path";
77886
+ async function queriesCopy(src, dst, options = {}) {
77887
+ if (!src.startsWith("@") || !dst.startsWith("@")) {
77888
+ throw new Error(`Both names must start with '@'`);
77889
+ }
77890
+ const cwd = options.cwd ?? process.cwd();
77891
+ const dirs = resolveSnippetDirs(cwd);
77892
+ if (options.builtinDirOverride)
77893
+ dirs.builtinDir = options.builtinDirOverride;
77894
+ const map = await loadSnippets(dirs);
77895
+ const variants = map.get(src);
77896
+ if (!variants || variants.length === 0)
77897
+ throw new Error(`Snippet not found: ${src}`);
77898
+ if (map.get(dst)?.some((v) => v.query.source === "local")) {
77899
+ throw new Error(`Target already exists: ${dst}`);
77900
+ }
77901
+ for (const v of variants) {
77902
+ const dstFile = mapEngineSuffix(v.query.file, snippetKeyToFile(cwd, dst, "local"));
77903
+ await mkdir2(dirname2(dstFile), { recursive: true });
77904
+ await copyFile(v.query.file, dstFile);
77905
+ console.log(`copied ${v.query.file} \u2192 ${dstFile}`);
77906
+ }
77907
+ }
77908
+ function mapEngineSuffix(srcFile, baseDst) {
77909
+ const m = basename2(srcFile).match(/\.(postgres|mysql)\.sql$/);
77910
+ return m ? baseDst.replace(/\.sql$/, `.${m[1]}.sql`) : baseDst;
77911
+ }
77912
+ var init_queries_copy = __esm(() => {
77913
+ init_saved_queries();
77914
+ });
77915
+
77916
+ // src/commands/queries-import.ts
77917
+ var exports_queries_import = {};
77918
+ __export(exports_queries_import, {
77919
+ queriesImport: () => queriesImport
77920
+ });
77921
+ import { stat, mkdir as mkdir3, copyFile as copyFile2 } from "fs/promises";
77922
+ import { basename as basename3, join as join12, extname } from "path";
77923
+ async function queriesImport(filePath, options = {}) {
77924
+ const cwd = options.cwd ?? process.cwd();
77925
+ if (extname(filePath) !== ".sql") {
77926
+ throw new Error(`Expected .sql file, got ${filePath}`);
77927
+ }
77928
+ await stat(filePath);
77929
+ const text2 = await Bun.file(filePath).text();
77930
+ const baseName = options.as ? options.as.replace(/^@/, "") : basename3(filePath, ".sql").replace(/\.(postgres|mysql)$/, "");
77931
+ const key2 = "@" + baseName;
77932
+ parseSavedQuery({ key: key2, file: filePath, source: "local", text: text2 });
77933
+ const targetDir = join12(cwd, ".dbcli/queries");
77934
+ await mkdir3(targetDir, { recursive: true });
77935
+ const target = join12(targetDir, basename3(filePath));
77936
+ if (await Bun.file(target).exists()) {
77937
+ if (!options.force) {
77938
+ const ok = await esm_default4({ message: `Overwrite ${target}?`, default: false });
77939
+ if (!ok)
77940
+ return;
77941
+ }
77942
+ }
77943
+ await copyFile2(filePath, target);
77944
+ console.log(`imported ${filePath} \u2192 ${target}`);
77945
+ }
77946
+ var init_queries_import = __esm(() => {
77947
+ init_esm14();
77948
+ init_saved_queries();
77949
+ });
77950
+
77951
+ // src/commands/queries-export.ts
77952
+ var exports_queries_export = {};
77953
+ __export(exports_queries_export, {
77954
+ queriesExport: () => queriesExport
77955
+ });
77956
+ import { writeFile } from "fs/promises";
77957
+ async function queriesExport(name, options = {}) {
77958
+ const cwd = options.cwd ?? process.cwd();
77959
+ const dirs = resolveSnippetDirs(cwd);
77960
+ const map = await loadSnippets(dirs);
77961
+ const variants = map.get(name);
77962
+ if (!variants || variants.length === 0)
77963
+ throw new Error(`Snippet not found: ${name}`);
77964
+ let chosen = variants;
77965
+ if (options.engine) {
77966
+ chosen = variants.filter((v) => (v.query.meta.engine ?? []).includes(options.engine));
77967
+ if (chosen.length === 0) {
77968
+ throw new Error(`No variant of ${name} matches engine ${options.engine}`);
77969
+ }
77970
+ }
77971
+ if (chosen.length > 1) {
77972
+ const engines = chosen.flatMap((v) => v.query.meta.engine ?? []).join(", ");
77973
+ throw new Error(`Snippet ${name} has multiple engine variants (${engines}); pass --engine to pick one.`);
77974
+ }
77975
+ const text2 = await Bun.file(chosen[0].query.file).text();
77976
+ if (options.output) {
77977
+ await writeFile(options.output, text2, "utf8");
77978
+ console.log(`wrote ${options.output}`);
77979
+ } else {
77980
+ process.stdout.write(text2);
77981
+ }
77982
+ }
77983
+ var init_queries_export = __esm(() => {
77984
+ init_saved_queries();
77985
+ });
77986
+
77131
77987
  // node_modules/commander/esm.mjs
77132
77988
  var import__ = __toESM(require_commander(), 1);
77133
77989
  var {
@@ -77146,7 +78002,7 @@ var {
77146
78002
  // package.json
77147
78003
  var package_default = {
77148
78004
  name: "@carllee1983/dbcli",
77149
- version: "1.6.0",
78005
+ version: "1.7.0",
77150
78006
  description: "Database CLI for AI agents",
77151
78007
  type: "module",
77152
78008
  publishConfig: {
@@ -77195,6 +78051,7 @@ var package_default = {
77195
78051
  "test:unit": "bun test tests/unit tests/core",
77196
78052
  "test:integration": "bun test tests/integration",
77197
78053
  "test:docker": "docker compose -f docker-compose.test.yml up -d --wait && bun test tests/integration/adapters; docker compose -f docker-compose.test.yml down",
78054
+ typecheck: "tsc --noEmit --pretty false",
77198
78055
  "test:run": "vitest --run",
77199
78056
  "test:bench": "vitest bench --run",
77200
78057
  bench: "bun run test:bench",
@@ -77205,7 +78062,6 @@ var package_default = {
77205
78062
  dependencies: {
77206
78063
  "cli-table3": "^0.6.3",
77207
78064
  commander: "13.0.0",
77208
- dotenv: "^16.3.1",
77209
78065
  mongodb: "^7.2.0",
77210
78066
  mysql2: "^3.20.0",
77211
78067
  pg: "^8.20.0",
@@ -77294,6 +78150,31 @@ var messages_default = {
77294
78150
  no_results: "No results returned",
77295
78151
  result_count: "Results: {count} row(s)"
77296
78152
  },
78153
+ q: {
78154
+ description: "Run a saved query snippet by @name",
78155
+ missing_param: "Missing required parameters for snippet '{name}'",
78156
+ engine_mismatch: "Engine mismatch for snippet '{name}'",
78157
+ snippet_not_found: "Snippet not found: {name}",
78158
+ dry_run_header: "Dry-run preview (no execution):"
78159
+ },
78160
+ queries: {
78161
+ description: "Manage saved query snippets",
78162
+ list_description: "List saved snippets",
78163
+ show_description: "Show a snippet (frontmatter + SQL)",
78164
+ new_description: "Create a new snippet file",
78165
+ edit_description: "Open a snippet in $EDITOR",
78166
+ check_description: "Validate all snippets",
78167
+ delete_description: "Delete a local snippet file",
78168
+ rename_description: "Rename a local snippet",
78169
+ copy_description: "Copy a snippet from any tier into local",
78170
+ import_description: "Import an external .sql file into local",
78171
+ export_description: "Export a snippet's raw SQL to stdout or file",
78172
+ created: "Created {file}",
78173
+ first_run_hint: "Tip: commit .dbcli-shared/ to share snippets with your team.",
78174
+ no_snippets: "No snippets found. Create one with: dbcli queries new @<name>",
78175
+ check_ok: "\u2713 {count} snippets parsed successfully",
78176
+ check_failed: "\u2717 {count} snippet(s) have errors"
78177
+ },
77297
78178
  errors: {
77298
78179
  message: "Error: {message}",
77299
78180
  invalid_config: "Invalid configuration: {field}",
@@ -77485,6 +78366,31 @@ var messages_default2 = {
77485
78366
  no_results: "\u672A\u8FD4\u56DE\u4EFB\u4F55\u7D50\u679C",
77486
78367
  result_count: "\u7D50\u679C\uFF1A{count} \u5217"
77487
78368
  },
78369
+ q: {
78370
+ description: "\u4F9D @name \u57F7\u884C\u5DF2\u4FDD\u5B58\u7684\u67E5\u8A62\u7247\u6BB5",
78371
+ missing_param: "\u7247\u6BB5 '{name}' \u7F3A\u5C11\u5FC5\u8981\u53C3\u6578",
78372
+ engine_mismatch: "\u7247\u6BB5 '{name}' \u5F15\u64CE\u4E0D\u7B26",
78373
+ snippet_not_found: "\u627E\u4E0D\u5230\u7247\u6BB5\uFF1A{name}",
78374
+ dry_run_header: "Dry-run \u9810\u89BD\uFF08\u4E0D\u6703\u57F7\u884C\uFF09\uFF1A"
78375
+ },
78376
+ queries: {
78377
+ description: "\u7BA1\u7406\u5DF2\u4FDD\u5B58\u7684\u67E5\u8A62\u7247\u6BB5",
78378
+ list_description: "\u5217\u51FA\u6240\u6709\u7247\u6BB5",
78379
+ show_description: "\u986F\u793A\u7247\u6BB5\u5167\u5BB9\uFF08frontmatter + SQL\uFF09",
78380
+ new_description: "\u5EFA\u7ACB\u65B0\u7684\u7247\u6BB5\u6A94",
78381
+ edit_description: "\u4EE5 $EDITOR \u958B\u555F\u7247\u6BB5",
78382
+ check_description: "\u9A57\u8B49\u6240\u6709\u7247\u6BB5",
78383
+ delete_description: "\u522A\u9664 local snippet \u6A94\u6848",
78384
+ rename_description: "\u91CD\u65B0\u547D\u540D local snippet",
78385
+ copy_description: "\u5C07\u4EFB\u4E00\u5C64\u7D1A\u7684 snippet \u8907\u88FD\u5230 local",
78386
+ import_description: "\u532F\u5165\u5916\u90E8 .sql \u6A94\u6848\u5230 local",
78387
+ export_description: "\u5C07 snippet \u539F\u59CB SQL \u8F38\u51FA\u5230 stdout \u6216\u6A94\u6848",
78388
+ created: "\u5DF2\u5EFA\u7ACB {file}",
78389
+ first_run_hint: "\u63D0\u793A\uFF1A\u5C07 .dbcli-shared/ commit \u9032\u7248\u63A7\u53EF\u8207\u5718\u968A\u5171\u4EAB\u7247\u6BB5\u3002",
78390
+ no_snippets: "\u5C1A\u7121\u4EFB\u4F55\u7247\u6BB5\u3002\u57F7\u884C dbcli queries new @<name> \u5EFA\u7ACB\u3002",
78391
+ check_ok: "\u2713 \u6210\u529F\u89E3\u6790 {count} \u500B\u7247\u6BB5",
78392
+ check_failed: "\u2717 \u6709 {count} \u500B\u7247\u6BB5\u89E3\u6790\u5931\u6557"
78393
+ },
77488
78394
  errors: {
77489
78395
  message: "\u932F\u8AA4\uFF1A{message}",
77490
78396
  invalid_config: "\u914D\u7F6E\u7121\u6548\uFF1A{field}",
@@ -78178,8 +79084,8 @@ var configModule = {
78178
79084
  }
78179
79085
  let schema = (v2Config.schemas ?? {})[resolved.name] ?? v2Config.schema;
78180
79086
  try {
78181
- const { SchemaLayeredLoader: SchemaLayeredLoader3 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
78182
- const loader = new SchemaLayeredLoader3(storagePath, { connectionName: resolved.name });
79087
+ const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
79088
+ const loader = new SchemaLayeredLoader2(storagePath, { connectionName: resolved.name });
78183
79089
  const { cache, index } = await loader.initialize();
78184
79090
  if (index && Object.keys(index.tables).length > 0) {
78185
79091
  const layeredSchema = {};
@@ -78192,7 +79098,7 @@ var configModule = {
78192
79098
  schema = layeredSchema;
78193
79099
  }
78194
79100
  }
78195
- } catch (error) {
79101
+ } catch {
78196
79102
  console.warn("Warning: Failed to load layered schema cache, falling back to config.json");
78197
79103
  }
78198
79104
  return DbcliConfigSchema.parse({
@@ -78333,7 +79239,7 @@ async function readLineFromStdin(prompt = "") {
78333
79239
  process.stdin.pause();
78334
79240
  process.stdin.removeListener("data", onData);
78335
79241
  process.stdin.removeListener("end", onEnd);
78336
- resolve2(lines2[0].trim());
79242
+ resolve2((lines2[0] ?? "").trim());
78337
79243
  }
78338
79244
  };
78339
79245
  const onEnd = () => {
@@ -78352,7 +79258,7 @@ async function text(message, defaultValue) {
78352
79258
  return answer.trim() || defaultValue || "";
78353
79259
  }
78354
79260
  try {
78355
- const { text: inquirerText } = await Promise.resolve().then(() => (init_esm14(), exports_esm));
79261
+ const { input: inquirerText } = await Promise.resolve().then(() => (init_esm14(), exports_esm));
78356
79262
  return await inquirerText({ message, default: defaultValue });
78357
79263
  } catch {
78358
79264
  const displayMessage = defaultValue ? `${message} [${defaultValue}]: ` : `${message}: `;
@@ -78369,9 +79275,9 @@ async function select(message, choices) {
78369
79275
  const answer = await readLineFromStdin("Select option (number): ");
78370
79276
  const selectedIndex = parseInt(answer, 10) - 1;
78371
79277
  if (selectedIndex >= 0 && selectedIndex < choices.length) {
78372
- return choices[selectedIndex];
79278
+ return choices[selectedIndex] ?? choices[0] ?? "";
78373
79279
  }
78374
- return choices[0];
79280
+ return choices[0] ?? "";
78375
79281
  }
78376
79282
  try {
78377
79283
  const { select: inquirerSelect } = await Promise.resolve().then(() => (init_esm14(), exports_esm));
@@ -78384,9 +79290,9 @@ async function select(message, choices) {
78384
79290
  const answer = await readLineFromStdin("Select option (number): ");
78385
79291
  const selectedIndex = parseInt(answer, 10) - 1;
78386
79292
  if (selectedIndex >= 0 && selectedIndex < choices.length) {
78387
- return choices[selectedIndex];
79293
+ return choices[selectedIndex] ?? choices[0] ?? "";
78388
79294
  }
78389
- return choices[0];
79295
+ return choices[0] ?? "";
78390
79296
  }
78391
79297
  }
78392
79298
  async function confirm(message) {
@@ -78439,7 +79345,7 @@ function mapError(error, system, options) {
78439
79345
  `Verify network connectivity: ping ${options.host} -c 3`
78440
79346
  ]);
78441
79347
  }
78442
- if (errMsg.includes("authentication") || errMsg.includes("auth") || errMsg.includes("password") || errMsg.includes("access denied") || errMsg.includes("FATAL")) {
79348
+ if (errMsg.includes("authentication") || errMsg.includes("auth") || errMsg.includes("password") || errMsg.includes("access denied") || errMsg.includes("does not exist") || errMsg.includes("role") || errMsg.includes("user") || errMsg.includes("FATAL")) {
78443
79349
  return new ConnectionError("AUTH_FAILED", `Authentication failed \u2014 check your username or password`, [
78444
79350
  `Verify credentials: ${system === "postgresql" ? `psql -U ${options.user} -h ${options.host}` : `mysql -u ${options.user} -h ${options.host}`}`,
78445
79351
  `Check pg_hba.conf (PostgreSQL) or user privileges (MySQL)`,
@@ -78484,7 +79390,7 @@ function parseVersionSegments(version) {
78484
79390
  const match = version.match(/^(\d+(?:\.\d+)*)/);
78485
79391
  if (!match)
78486
79392
  return [];
78487
- return match[1].split(".").map(Number);
79393
+ return (match[1] ?? "").split(".").map(Number);
78488
79394
  }
78489
79395
  function isMariaDBVersion(versionString) {
78490
79396
  return /mariadb/i.test(versionString);
@@ -78492,10 +79398,10 @@ function isMariaDBVersion(versionString) {
78492
79398
  function extractMariaDBVersion(versionString) {
78493
79399
  const match = versionString.match(/(\d+\.\d+\.\d+)-MariaDB/i);
78494
79400
  if (match)
78495
- return match[1];
79401
+ return match[1] ?? versionString;
78496
79402
  const prefixMatch = versionString.match(/^5\.5\.5-(\d+\.\d+\.\d+)/);
78497
79403
  if (prefixMatch)
78498
- return prefixMatch[1];
79404
+ return prefixMatch[1] ?? versionString;
78499
79405
  return versionString;
78500
79406
  }
78501
79407
  function compareVersions(a, b) {
@@ -78514,8 +79420,8 @@ function checkDbVersion(rawVersion, declaredSystem) {
78514
79420
  const system = isMariaDB ? "mariadb" : declaredSystem;
78515
79421
  const serverVersion = isMariaDB ? extractMariaDBVersion(rawVersion) : rawVersion;
78516
79422
  const minVersion = MIN_SUPPORTED_VERSIONS[system];
78517
- const supported = compareVersions(serverVersion, minVersion) >= 0;
78518
- return { serverVersion, system, supported, minVersion };
79423
+ const supported = compareVersions(serverVersion, minVersion ?? "0") >= 0;
79424
+ return { serverVersion, system, supported, minVersion: minVersion ?? "0" };
78519
79425
  }
78520
79426
  function warnIfUnsupported(result) {
78521
79427
  if (result.supported)
@@ -78862,7 +79768,7 @@ function parseEnumValues(columnType) {
78862
79768
  const match = columnType.match(/^enum\((.+)\)$/i);
78863
79769
  if (!match)
78864
79770
  return;
78865
- return match[1].split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
79771
+ return (match[1] ?? "").split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
78866
79772
  }
78867
79773
 
78868
79774
  class MySQLAdapter {
@@ -79139,7 +80045,7 @@ class MongoDBAdapter {
79139
80045
  });
79140
80046
  } catch (error) {
79141
80047
  const code = error?.code;
79142
- if (code !== "ECONNREFUSED") {
80048
+ if (!["ECONNREFUSED", "ENOTFOUND", "ETIMEOUT", "ETIMEDOUT"].includes(code ?? "")) {
79143
80049
  throw error;
79144
80050
  }
79145
80051
  }
@@ -79164,7 +80070,7 @@ class MongoDBAdapter {
79164
80070
  return this.parseTxtRecords(records);
79165
80071
  } catch (error) {
79166
80072
  const code = error?.code;
79167
- if (code !== "ECONNREFUSED") {
80073
+ if (!["ECONNREFUSED", "ENOTFOUND", "ETIMEOUT", "ETIMEDOUT"].includes(code ?? "")) {
79168
80074
  throw error;
79169
80075
  }
79170
80076
  }
@@ -79294,7 +80200,7 @@ class MongoDBAdapter {
79294
80200
  name: collectionName,
79295
80201
  columns,
79296
80202
  estimatedRowCount,
79297
- tableType: "collection"
80203
+ tableType: "table"
79298
80204
  };
79299
80205
  }
79300
80206
  async testConnection() {
@@ -79358,7 +80264,7 @@ class AdapterFactory {
79358
80264
  }
79359
80265
  // src/utils/config-path.ts
79360
80266
  function resolveConfigPath(command, options, fallback = ".dbcli") {
79361
- for (let current = command;current; current = current.parent) {
80267
+ for (let current = command;current; current = current.parent ?? undefined) {
79362
80268
  const source = current.getOptionValueSource("config");
79363
80269
  if (source && source !== "default") {
79364
80270
  const currentOptions = current.opts();
@@ -80343,7 +81249,10 @@ class SchemaWriter {
80343
81249
  const mapping = SchemaIndexBuilder.calculateFileMapping(index);
80344
81250
  const hotSchemas = {};
80345
81251
  for (const item of mapping.hot) {
80346
- hotSchemas[item.table] = schema[item.table];
81252
+ const tableSchema = schema[item.table];
81253
+ if (tableSchema) {
81254
+ hotSchemas[item.table] = tableSchema;
81255
+ }
80347
81256
  }
80348
81257
  await this.writer.writeJSON(join9(schemaRoot, "hot-schemas.json"), hotSchemas);
80349
81258
  const coldGroups = {};
@@ -80351,7 +81260,10 @@ class SchemaWriter {
80351
81260
  if (!coldGroups[item.file]) {
80352
81261
  coldGroups[item.file] = {};
80353
81262
  }
80354
- coldGroups[item.file][item.table] = schema[item.table];
81263
+ const tableSchema = schema[item.table];
81264
+ if (tableSchema) {
81265
+ coldGroups[item.file][item.table] = tableSchema;
81266
+ }
80355
81267
  }
80356
81268
  const coldDir = join9(schemaRoot, "cold");
80357
81269
  await this.ensureDir(coldDir);
@@ -80425,7 +81337,7 @@ class ColumnIndexBuilder {
80425
81337
  findColumnsMatching(pattern) {
80426
81338
  const regex = typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
80427
81339
  const results = [];
80428
- for (const [colName, entry] of this.index.columns.entries()) {
81340
+ for (const [, entry] of this.index.columns.entries()) {
80429
81341
  if (regex.test(entry.name)) {
80430
81342
  results.push({
80431
81343
  columnName: entry.name,
@@ -80670,7 +81582,7 @@ class HealthChecker {
80670
81582
  const blacklisted = options.blacklistedColumns || new Set;
80671
81583
  const visibleColumns = schema.columns.filter((c) => !blacklisted.has(`${schema.name}.${c.name}`));
80672
81584
  const countResult = await this.adapter.execute(`SELECT COUNT(*) as count FROM \`${schema.name}\``);
80673
- const rowCount = countResult[0]?.count || 0;
81585
+ const rowCount = rowsOf(countResult)[0]?.count || 0;
80674
81586
  const nulls = checks.includes("nulls") ? await this.checkNulls(schema.name, visibleColumns, rowCount, sample2) : [];
80675
81587
  const orphans = checks.includes("orphans") ? await this.checkOrphans(schema.name, visibleColumns) : [];
80676
81588
  const duplicates = checks.includes("duplicates") ? await this.checkDuplicates(schema.name, schema.indexes || []) : [];
@@ -80697,7 +81609,7 @@ class HealthChecker {
80697
81609
  const useSample = totalRows > sample2;
80698
81610
  const sql = useSample ? `SELECT COUNT(*) - COUNT(\`${col.name}\`) as null_count FROM (SELECT \`${col.name}\` FROM \`${tableName}\` LIMIT ${sample2}) sub` : `SELECT COUNT(*) - COUNT(\`${col.name}\`) as null_count FROM \`${tableName}\``;
80699
81611
  const result = await this.adapter.execute(sql);
80700
- const nullCount = result[0]?.null_count || 0;
81612
+ const nullCount = rowsOf(result)[0]?.null_count || 0;
80701
81613
  if (nullCount > 0) {
80702
81614
  const sampleSize = Math.min(totalRows, sample2);
80703
81615
  results.push({
@@ -80726,7 +81638,7 @@ class HealthChecker {
80726
81638
  AND parent.\`${col.foreignKey.column}\` IS NULL
80727
81639
  `;
80728
81640
  const result = await this.adapter.execute(sql);
80729
- const orphanCount = result[0]?.orphan_count || 0;
81641
+ const orphanCount = rowsOf(result)[0]?.orphan_count || 0;
80730
81642
  if (orphanCount > 0) {
80731
81643
  results.push({
80732
81644
  column: col.name,
@@ -80753,7 +81665,7 @@ class HealthChecker {
80753
81665
  ) dups
80754
81666
  `;
80755
81667
  const result = await this.adapter.execute(sql);
80756
- const dupCount = result[0]?.dup_count || 0;
81668
+ const dupCount = rowsOf(result)[0]?.dup_count || 0;
80757
81669
  if (dupCount > 0) {
80758
81670
  results.push({
80759
81671
  columns: idx.columns,
@@ -80772,7 +81684,7 @@ class HealthChecker {
80772
81684
  try {
80773
81685
  const sql = `SELECT COUNT(*) as empty_count FROM \`${tableName}\` WHERE \`${col.name}\` = ''`;
80774
81686
  const result = await this.adapter.execute(sql);
80775
- const count = result[0]?.empty_count || 0;
81687
+ const count = rowsOf(result)[0]?.empty_count || 0;
80776
81688
  if (count > 0) {
80777
81689
  results.push({ column: col.name, count });
80778
81690
  }
@@ -80781,6 +81693,13 @@ class HealthChecker {
80781
81693
  return results;
80782
81694
  }
80783
81695
  }
81696
+ function rowsOf(result) {
81697
+ return Array.isArray(result) ? result : result.rows;
81698
+ }
81699
+
81700
+ // src/core/index.ts
81701
+ init_schema_loader();
81702
+
80784
81703
  // src/commands/schema.ts
80785
81704
  init_validation();
80786
81705
  var ALLOWED_FORMATS2 = ["table", "json"];
@@ -80919,8 +81838,8 @@ async function handleSchemaRefresh(adapter, config, options, connectionName, sto
80919
81838
  }
80920
81839
  async function handleSchemaReset(adapter, config, options, connectionName, existingCount, storagePath) {
80921
81840
  if (existingCount > 0 && !options.force) {
80922
- const { SchemaLayeredLoader: SchemaLayeredLoader3 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
80923
- const loader = new SchemaLayeredLoader3(storagePath, { connectionName });
81841
+ const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
81842
+ const loader = new SchemaLayeredLoader2(storagePath, { connectionName });
80924
81843
  const { index } = await loader.initialize();
80925
81844
  if (!index || Object.keys(index.tables).length === 0) {
80926
81845
  console.log(`\u26A0 This will clear ${existingCount} existing table schemas and re-fetch from database.`);
@@ -81010,8 +81929,8 @@ async function handleFullDatabaseScan(adapter, config, options, connectionName,
81010
81929
  }
81011
81930
  }
81012
81931
  if (existingSchemaCount > 0 && !options.force) {
81013
- const { SchemaLayeredLoader: SchemaLayeredLoader3 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
81014
- const loader = new SchemaLayeredLoader3(storagePath, { connectionName });
81932
+ const { SchemaLayeredLoader: SchemaLayeredLoader2 } = await Promise.resolve().then(() => (init_schema_loader(), exports_schema_loader));
81933
+ const loader = new SchemaLayeredLoader2(storagePath, { connectionName });
81015
81934
  const { index } = await loader.initialize();
81016
81935
  if (!index || Object.keys(index.tables).length === 0) {
81017
81936
  console.log(`
@@ -81222,7 +82141,7 @@ function extractAllKeywords(sql) {
81222
82141
  }
81223
82142
  return Array.from(keywords).sort();
81224
82143
  }
81225
- function determineConfidence(type, keyword, sql) {
82144
+ function determineConfidence(type, _keyword, _sql) {
81226
82145
  const highConfidenceTypes = [
81227
82146
  "SELECT",
81228
82147
  "INSERT",
@@ -81329,49 +82248,6 @@ function enforcePermission(sql, permission) {
81329
82248
  return result.classification;
81330
82249
  }
81331
82250
 
81332
- // src/utils/levenshtein-distance.ts
81333
- function levenshteinDistance(a, b) {
81334
- const lenA = a.length;
81335
- const lenB = b.length;
81336
- const dp = [];
81337
- for (let i = 0;i <= lenB; i++) {
81338
- const row = [];
81339
- for (let j = 0;j <= lenA; j++) {
81340
- row[j] = 0;
81341
- }
81342
- dp[i] = row;
81343
- }
81344
- for (let i = 0;i <= lenB; i++) {
81345
- const row = dp[i];
81346
- if (row !== undefined) {
81347
- row[0] = i;
81348
- }
81349
- }
81350
- for (let j = 0;j <= lenA; j++) {
81351
- const firstRow = dp[0];
81352
- if (firstRow !== undefined) {
81353
- firstRow[j] = j;
81354
- }
81355
- }
81356
- for (let i = 1;i <= lenB; i++) {
81357
- const currentRow = dp[i];
81358
- const prevRow = dp[i - 1];
81359
- if (!currentRow || !prevRow)
81360
- continue;
81361
- for (let j = 1;j <= lenA; j++) {
81362
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
81363
- currentRow[j] = prevRow[j - 1] ?? 0;
81364
- } else {
81365
- const sub = (prevRow[j - 1] ?? 0) + 1;
81366
- const ins = (currentRow[j - 1] ?? 0) + 1;
81367
- const del = (prevRow[j] ?? 0) + 1;
81368
- currentRow[j] = Math.min(sub, ins, del);
81369
- }
81370
- }
81371
- }
81372
- return dp[lenB]?.[lenA] ?? 0;
81373
- }
81374
-
81375
82251
  // src/utils/error-suggester.ts
81376
82252
  async function suggestTableName(errorMessage, adapter) {
81377
82253
  try {
@@ -81468,7 +82344,7 @@ class QueryExecutor {
81468
82344
  const executionTimeMs = Math.round(performance.now() - start);
81469
82345
  const rows = resultData.rows;
81470
82346
  const affectedRows = resultData.affectedRows;
81471
- let columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
82347
+ let columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
81472
82348
  const columnTypes = columnNames.map((col) => {
81473
82349
  const value = rows[0]?.[col];
81474
82350
  return inferColumnType(value);
@@ -81513,7 +82389,7 @@ class QueryExecutor {
81513
82389
  const enhancedError = new Error(`${errorMessage}
81514
82390
  ` + (suggestions.length > 0 ? `Did you mean: ${suggestions.join(", ")}?` : `Available tables: ${tables.slice(0, 5).join(", ")}...`));
81515
82391
  throw enhancedError;
81516
- } catch (suggestionError) {
82392
+ } catch {
81517
82393
  throw error;
81518
82394
  }
81519
82395
  }
@@ -81524,19 +82400,19 @@ class QueryExecutor {
81524
82400
  function extractTableName(sql) {
81525
82401
  const match = sql.match(/\bFROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
81526
82402
  if (match) {
81527
- return match[1];
82403
+ return match[1] ?? null;
81528
82404
  }
81529
82405
  const insertMatch = sql.match(/\bINSERT\s+INTO\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
81530
82406
  if (insertMatch) {
81531
- return insertMatch[1];
82407
+ return insertMatch[1] ?? null;
81532
82408
  }
81533
82409
  const updateMatch = sql.match(/\bUPDATE\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
81534
82410
  if (updateMatch) {
81535
- return updateMatch[1];
82411
+ return updateMatch[1] ?? null;
81536
82412
  }
81537
82413
  const deleteMatch = sql.match(/\bDELETE\s+FROM\s+["'`]?([a-zA-Z_][a-zA-Z0-9_]*)["'`]?/i);
81538
82414
  if (deleteMatch) {
81539
- return deleteMatch[1];
82415
+ return deleteMatch[1] ?? null;
81540
82416
  }
81541
82417
  return null;
81542
82418
  }
@@ -81689,7 +82565,9 @@ async function mongoQueryBranch(queryStr, options, config) {
81689
82565
  columnNames: columnNames.filter((col) => !filterResult.omittedColumns.includes(col))
81690
82566
  };
81691
82567
  const formatter = new QueryResultFormatter;
81692
- const output = formatter.format(queryResult, { format });
82568
+ const output = formatter.format(queryResult, {
82569
+ format
82570
+ });
81693
82571
  const securityNote = blacklistValidator.buildSecurityNotification(collection, filterResult.omittedColumns);
81694
82572
  console.log(output);
81695
82573
  if (securityNote) {
@@ -81701,6 +82579,794 @@ async function mongoQueryBranch(queryStr, options, config) {
81701
82579
  }
81702
82580
  }
81703
82581
 
82582
+ // src/core/query-risk-analyzer.ts
82583
+ init_size_category();
82584
+
82585
+ // src/types/query-risk.ts
82586
+ function mapStatementTypeToRiskOperation(type) {
82587
+ if (type === "DROP" || type === "TRUNCATE" || type === "ALTER" || type === "CREATE") {
82588
+ return "DDL";
82589
+ }
82590
+ if (type === "SELECT" || type === "INSERT" || type === "UPDATE" || type === "DELETE" || type === "SHOW" || type === "DESCRIBE" || type === "EXPLAIN") {
82591
+ return type;
82592
+ }
82593
+ return "UNKNOWN";
82594
+ }
82595
+
82596
+ // src/core/query-risk-analyzer.ts
82597
+ function stripCommentsAndSingleQuotedStrings(sql) {
82598
+ let result = "";
82599
+ let i = 0;
82600
+ while (i < sql.length) {
82601
+ const char = sql[i];
82602
+ if (char === "-" && sql[i + 1] === "-") {
82603
+ while (i < sql.length && sql[i] !== `
82604
+ `)
82605
+ i++;
82606
+ if (i < sql.length) {
82607
+ result += `
82608
+ `;
82609
+ i++;
82610
+ }
82611
+ continue;
82612
+ }
82613
+ if (char === "/" && sql[i + 1] === "*") {
82614
+ i += 2;
82615
+ while (i < sql.length) {
82616
+ if (sql[i] === "*" && sql[i + 1] === "/") {
82617
+ i += 2;
82618
+ break;
82619
+ }
82620
+ i++;
82621
+ }
82622
+ result += " ";
82623
+ continue;
82624
+ }
82625
+ if (char === "'") {
82626
+ i++;
82627
+ while (i < sql.length && sql[i] !== "'") {
82628
+ if (sql[i] === "\\")
82629
+ i++;
82630
+ i++;
82631
+ }
82632
+ if (i < sql.length)
82633
+ i++;
82634
+ result += " ";
82635
+ continue;
82636
+ }
82637
+ result += char;
82638
+ i++;
82639
+ }
82640
+ return result;
82641
+ }
82642
+ var READ_LIKE_WORDS = ["SELECT", "SHOW", "DESCRIBE", "EXPLAIN", "WITH"];
82643
+ var WRITE_OR_DDL_LIKE_WORDS = [
82644
+ "INSERT",
82645
+ "UPDATE",
82646
+ "DELETE",
82647
+ "MERGE",
82648
+ "UPSERT",
82649
+ "DROP",
82650
+ "TRUNCATE",
82651
+ "ALTER",
82652
+ "CREATE",
82653
+ "RENAME",
82654
+ "GRANT",
82655
+ "REVOKE"
82656
+ ];
82657
+ function analyzeQueryRisk(input) {
82658
+ const facts = collectSqlFacts(input.sql);
82659
+ const factors = [];
82660
+ applyPermissionRules(input.sql, input.permission, factors);
82661
+ applyWriteWhereRules(facts, factors);
82662
+ applyDdlAndUnknownRules(facts, factors);
82663
+ applySelectPatternRules(facts, factors);
82664
+ applyBlacklistRules(facts, input.blacklist, factors);
82665
+ const schemaGap = applySchemaRules(facts, input.schemaLookup, factors);
82666
+ const recommendations = buildRecommendations(factors, facts);
82667
+ const suggestedCommands = buildSuggestedCommands(facts, schemaGap);
82668
+ return {
82669
+ decision: decide(factors),
82670
+ operation: facts.operation,
82671
+ targetTables: facts.targetTables,
82672
+ riskFactors: factors,
82673
+ recommendations,
82674
+ suggestedCommands
82675
+ };
82676
+ }
82677
+ function collectSqlFacts(sql) {
82678
+ const normalizedSql = normalizeSql(sql);
82679
+ const analysisSql = normalizeSql(stripCommentsAndSingleQuotedStrings(sql));
82680
+ const classification = classifyStatement(sql);
82681
+ const operation = mapStatementTypeToRiskOperation(classification.type);
82682
+ return {
82683
+ normalizedSql,
82684
+ analysisSql,
82685
+ operation,
82686
+ targetTables: extractTargetTables(analysisSql, operation),
82687
+ referencedColumns: extractReferencedColumns(analysisSql, operation),
82688
+ hasWhere: /\bWHERE\b/i.test(analysisSql),
82689
+ hasLimit: /\bLIMIT\b/i.test(analysisSql),
82690
+ hasSelectStar: /^\s*SELECT\s+(?:DISTINCT\s+)?\*/i.test(analysisSql),
82691
+ appearsReadLike: READ_LIKE_WORDS.some((word) => new RegExp(`\\b${word}\\b`, "i").test(analysisSql)),
82692
+ appearsWriteOrDdlLike: WRITE_OR_DDL_LIKE_WORDS.some((word) => new RegExp(`\\b${word}\\b`, "i").test(analysisSql))
82693
+ };
82694
+ }
82695
+ function normalizeSql(sql) {
82696
+ return sql.trim().replace(/\s+/g, " ");
82697
+ }
82698
+ function extractTargetTables(sql, operation) {
82699
+ const cteAliases = extractCteAliases(sql);
82700
+ const tables = new Set;
82701
+ const add = (value) => {
82702
+ const cleaned = cleanIdentifier(value);
82703
+ if (!cleaned)
82704
+ return;
82705
+ if (cteAliases.has(cleaned.toLowerCase()))
82706
+ return;
82707
+ tables.add(cleaned);
82708
+ };
82709
+ if (operation === "UPDATE") {
82710
+ add(sql.match(/\bUPDATE\s+([`"\[]?[\w.]+[`"\]]?)/i)?.[1]);
82711
+ }
82712
+ if (operation === "DELETE") {
82713
+ add(sql.match(/\bDELETE\s+FROM\s+([`"\[]?[\w.]+[`"\]]?)/i)?.[1]);
82714
+ }
82715
+ if (operation === "INSERT") {
82716
+ add(sql.match(/\bINSERT\s+INTO\s+([`"\[]?[\w.]+[`"\]]?)/i)?.[1]);
82717
+ }
82718
+ const fromJoinPattern = /\b(?:FROM|JOIN)\s+([`"\[]?[\w.]+[`"\]]?)/gi;
82719
+ for (const match of sql.matchAll(fromJoinPattern)) {
82720
+ add(match[1]);
82721
+ }
82722
+ return Array.from(tables);
82723
+ }
82724
+ function extractCteAliases(sql) {
82725
+ const aliases = new Set;
82726
+ const withMatch = sql.match(/\bWITH\b/i);
82727
+ if (!withMatch || withMatch.index === undefined)
82728
+ return aliases;
82729
+ const tail = sql.slice(withMatch.index + withMatch[0].length);
82730
+ const aliasPattern = /([`"\[]?[\w]+[`"\]]?)\s+AS\s*\(/gi;
82731
+ for (const match of tail.matchAll(aliasPattern)) {
82732
+ const cleaned = cleanIdentifier(match[1]);
82733
+ if (cleaned)
82734
+ aliases.add(cleaned.toLowerCase());
82735
+ }
82736
+ return aliases;
82737
+ }
82738
+ function cleanIdentifier(value) {
82739
+ if (!value)
82740
+ return null;
82741
+ const trimmed = value.trim().replace(/^[`"\[]|[`"\]]$/g, "");
82742
+ const withoutSchema = trimmed.includes(".") ? trimmed.split(".").slice(-1)[0] : trimmed;
82743
+ if (!withoutSchema)
82744
+ return null;
82745
+ if (/^(SELECT|WHERE|JOIN|ON|SET|VALUES)$/i.test(withoutSchema))
82746
+ return null;
82747
+ return withoutSchema;
82748
+ }
82749
+ function extractReferencedColumns(sql, operation) {
82750
+ const columns = new Set;
82751
+ const add = (value) => {
82752
+ const cleaned = cleanIdentifier(value);
82753
+ if (cleaned && cleaned !== "*")
82754
+ columns.add(cleaned);
82755
+ };
82756
+ if (operation === "SELECT") {
82757
+ const selectList = sql.match(/^\s*SELECT\s+(?:DISTINCT\s+)?(.+?)\s+FROM\s+/i)?.[1];
82758
+ if (selectList && selectList.trim() !== "*") {
82759
+ for (const part of selectList.split(",")) {
82760
+ const expression = part.trim().replace(/\s+AS\s+[`"\[]?[\w]+[`"\]]?$/i, "");
82761
+ const simpleColumn = expression.match(/^(?:[`"\[]?[\w]+[`"\]]?\.)?([`"\[]?[\w]+[`"\]]?)$/)?.[1];
82762
+ add(simpleColumn);
82763
+ }
82764
+ }
82765
+ }
82766
+ if (operation === "UPDATE") {
82767
+ const setList = sql.match(/\bSET\s+(.+?)(?:\s+WHERE\s+|$)/i)?.[1];
82768
+ if (setList) {
82769
+ for (const part of setList.split(",")) {
82770
+ add(part.match(/^\s*([`"\[]?[\w]+[`"\]]?)\s*=/)?.[1]);
82771
+ }
82772
+ }
82773
+ }
82774
+ if (operation === "INSERT") {
82775
+ const insertColumns = sql.match(/\bINSERT\s+INTO\s+[`"\[]?[\w.]+[`"\]]?\s*\(([^)]+)\)/i)?.[1];
82776
+ if (insertColumns) {
82777
+ for (const part of insertColumns.split(","))
82778
+ add(part.trim());
82779
+ }
82780
+ }
82781
+ return Array.from(columns);
82782
+ }
82783
+ function applyPermissionRules(sql, permission, factors) {
82784
+ const permissionResult = checkPermission(sql, permission);
82785
+ if (!permissionResult.allowed) {
82786
+ pushFactor(factors, "permission_denied", "block", permissionResult.reason);
82787
+ }
82788
+ }
82789
+ function applyWriteWhereRules(facts, factors) {
82790
+ if ((facts.operation === "UPDATE" || facts.operation === "DELETE") && !facts.hasWhere) {
82791
+ pushFactor(factors, "write_missing_where", "block", `${facts.operation} statement has no WHERE clause.`);
82792
+ }
82793
+ }
82794
+ function applyDdlAndUnknownRules(facts, factors) {
82795
+ if (/\b(DROP|TRUNCATE)\b/i.test(facts.analysisSql)) {
82796
+ pushFactor(factors, "destructive_ddl", "block", "Statement appears to contain destructive DDL.");
82797
+ return;
82798
+ }
82799
+ if (/\bALTER\b/i.test(facts.analysisSql)) {
82800
+ pushFactor(factors, "destructive_ddl", "block", "ALTER statements are unsupported by plan and may be destructive.");
82801
+ return;
82802
+ }
82803
+ if (facts.operation === "DDL") {
82804
+ pushFactor(factors, "unsupported_ddl", "block", "DDL statements are unsupported by plan.");
82805
+ return;
82806
+ }
82807
+ if (facts.operation === "UNKNOWN" && facts.appearsWriteOrDdlLike) {
82808
+ pushFactor(factors, "unknown_write_or_ddl", "block", "SQL type is unclear and appears write-like or DDL-like.");
82809
+ return;
82810
+ }
82811
+ if (facts.operation === "UNKNOWN" && facts.appearsReadLike) {
82812
+ pushFactor(factors, "unknown_read", "warn", "SQL type is unclear but appears read-like.");
82813
+ }
82814
+ }
82815
+ function applySelectPatternRules(facts, factors) {
82816
+ if (facts.operation === "SELECT" && facts.hasSelectStar) {
82817
+ pushFactor(factors, "select_star", "warn", "SELECT * may expose unnecessary columns.");
82818
+ }
82819
+ }
82820
+ function applyBlacklistRules(facts, blacklist, factors) {
82821
+ const blacklistedTables = new Set((blacklist.tables ?? []).map((table) => table.toLowerCase()));
82822
+ for (const table of facts.targetTables) {
82823
+ if (blacklistedTables.has(table.toLowerCase())) {
82824
+ pushFactor(factors, "table_blacklisted", "block", `Target table ${table} is blacklisted.`);
82825
+ }
82826
+ }
82827
+ for (const table of facts.targetTables) {
82828
+ const blacklistedColumns = findBlacklistedColumnsForTable(blacklist, table);
82829
+ for (const column of facts.referencedColumns) {
82830
+ if (blacklistedColumns.has(column)) {
82831
+ pushFactor(factors, "blacklisted_column", "warn", `Query references blacklisted column ${table}.${column}.`);
82832
+ }
82833
+ }
82834
+ }
82835
+ }
82836
+ function findBlacklistedColumnsForTable(blacklist, table) {
82837
+ const columns = blacklist.columns ?? {};
82838
+ for (const [tableName, columnNames] of Object.entries(columns)) {
82839
+ if (tableName.toLowerCase() === table.toLowerCase()) {
82840
+ return new Set(columnNames);
82841
+ }
82842
+ }
82843
+ return new Set;
82844
+ }
82845
+ function applySchemaRules(facts, schemaLookup, factors) {
82846
+ const gap = { cacheMissing: false, unknownTables: [] };
82847
+ if (facts.targetTables.length === 0)
82848
+ return gap;
82849
+ if (!schemaLookup.cacheAvailable) {
82850
+ pushFactor(factors, "schema_cache_missing", "warn", "Schema cache is missing for the selected connection.");
82851
+ gap.cacheMissing = true;
82852
+ return gap;
82853
+ }
82854
+ const knownTables = new Set(Object.keys(schemaLookup.tables).map((table) => table.toLowerCase()));
82855
+ let knownCount = 0;
82856
+ for (const table of facts.targetTables) {
82857
+ const schema = getSchemaForTable(schemaLookup.tables, table);
82858
+ if (!schema || !knownTables.has(table.toLowerCase())) {
82859
+ pushFactor(factors, "schema_table_unknown", "warn", `Target table ${table} is missing from schema cache.`);
82860
+ gap.unknownTables.push(table);
82861
+ continue;
82862
+ }
82863
+ knownCount += 1;
82864
+ const category = getSizeCategory(schema.estimatedRowCount ?? schema.rowCount ?? 0);
82865
+ if (facts.operation === "SELECT" && (category === "large" || category === "huge") && !facts.hasWhere && !facts.hasLimit) {
82866
+ pushFactor(factors, "large_table_unfiltered", "warn", `Target table ${table} is ${category} and the query has no WHERE or LIMIT clause.`);
82867
+ }
82868
+ }
82869
+ if (facts.targetTables.length > 1 && knownCount > 0 && knownCount < facts.targetTables.length) {
82870
+ pushFactor(factors, "partial_schema_coverage", "warn", "Multi-table query has partial schema-cache coverage.");
82871
+ }
82872
+ return gap;
82873
+ }
82874
+ function getSchemaForTable(tables, table) {
82875
+ return Object.entries(tables).find(([name]) => name.toLowerCase() === table.toLowerCase())?.[1];
82876
+ }
82877
+ function pushFactor(factors, code, severity, message) {
82878
+ if (factors.some((factor) => factor.code === code && factor.message === message))
82879
+ return;
82880
+ factors.push({ code, severity, message });
82881
+ }
82882
+ function decide(factors) {
82883
+ if (factors.some((factor) => factor.severity === "block"))
82884
+ return "BLOCK";
82885
+ if (factors.some((factor) => factor.severity === "warn"))
82886
+ return "WARN";
82887
+ return "ALLOW";
82888
+ }
82889
+ function buildRecommendations(factors, facts) {
82890
+ const recommendations = new Set;
82891
+ const codes = new Set(factors.map((factor) => factor.code));
82892
+ if (codes.has("write_missing_where")) {
82893
+ recommendations.add("Add a WHERE clause.");
82894
+ recommendations.add("Run a SELECT count(*) with the same condition before executing.");
82895
+ recommendations.add("Use --dry-run on the actual write command.");
82896
+ }
82897
+ if (codes.has("permission_denied")) {
82898
+ recommendations.add("Switch to a connection with sufficient permission only if the operation is intended.");
82899
+ }
82900
+ if (codes.has("table_blacklisted") || codes.has("blacklisted_column")) {
82901
+ recommendations.add("Review blacklist rules before accessing sensitive data.");
82902
+ }
82903
+ if (codes.has("destructive_ddl") || codes.has("unsupported_ddl") || codes.has("unknown_write_or_ddl")) {
82904
+ recommendations.add("Do not execute this statement through dbcli without manual review.");
82905
+ }
82906
+ if (codes.has("select_star")) {
82907
+ recommendations.add("Select only the columns required for the task.");
82908
+ }
82909
+ if (codes.has("large_table_unfiltered")) {
82910
+ recommendations.add("Add a WHERE clause or LIMIT before querying a large table.");
82911
+ }
82912
+ if (codes.has("schema_cache_missing") || codes.has("schema_table_unknown") || codes.has("partial_schema_coverage")) {
82913
+ recommendations.add("Refresh schema cache for the target table before executing.");
82914
+ }
82915
+ if (facts.operation === "UPDATE" || facts.operation === "DELETE" || facts.operation === "INSERT") {
82916
+ recommendations.add("Use --dry-run on the actual write command.");
82917
+ }
82918
+ return Array.from(recommendations);
82919
+ }
82920
+ function buildSuggestedCommands(facts, gap) {
82921
+ const commands = new Set;
82922
+ const targets = gap.cacheMissing ? facts.targetTables : gap.unknownTables;
82923
+ for (const table of targets) {
82924
+ commands.add(`dbcli schema ${table} --format json`);
82925
+ }
82926
+ return Array.from(commands);
82927
+ }
82928
+
82929
+ // src/commands/plan.ts
82930
+ init_validation();
82931
+ var ALLOWED_FORMATS4 = ["text", "json"];
82932
+ async function planCommand(sql, options, command) {
82933
+ try {
82934
+ if (!sql || sql.trim() === "") {
82935
+ throw new Error("SQL statement required");
82936
+ }
82937
+ const format = options.format ?? "text";
82938
+ validateFormat(format, ALLOWED_FORMATS4, "plan");
82939
+ const configPath = resolveConfigPath(command, options);
82940
+ const config = await configModule.read(configPath);
82941
+ if (!config.connection) {
82942
+ throw new Error('Run "dbcli init" first');
82943
+ }
82944
+ const schema = config.schema ?? {};
82945
+ const schemaTableCount = Object.keys(schema).length;
82946
+ const result = analyzeQueryRisk({
82947
+ sql: sql.trim(),
82948
+ permission: config.permission,
82949
+ blacklist: config.blacklist ?? { tables: [], columns: {} },
82950
+ schemaLookup: {
82951
+ tables: schema,
82952
+ cacheAvailable: schemaTableCount > 0
82953
+ }
82954
+ });
82955
+ console.log(formatPlanResult(result, format));
82956
+ } catch (error) {
82957
+ console.error(error.message);
82958
+ process.exit(1);
82959
+ }
82960
+ }
82961
+ function formatPlanResult(result, format) {
82962
+ if (format === "json") {
82963
+ return JSON.stringify(result, null, 2);
82964
+ }
82965
+ return formatPlanText(result);
82966
+ }
82967
+ function formatPlanText(result) {
82968
+ const lines2 = [
82969
+ `Decision: ${result.decision}`,
82970
+ `Operation: ${result.operation}`,
82971
+ `Target tables: ${result.targetTables.length > 0 ? result.targetTables.join(", ") : "(none)"}`
82972
+ ];
82973
+ if (result.riskFactors.length > 0) {
82974
+ lines2.push("", "Risk factors:");
82975
+ for (const factor of result.riskFactors) {
82976
+ lines2.push(`- ${factor.message}`);
82977
+ }
82978
+ }
82979
+ if (result.recommendations.length > 0) {
82980
+ lines2.push("", "Recommendations:");
82981
+ for (const recommendation of result.recommendations) {
82982
+ lines2.push(`- ${recommendation}`);
82983
+ }
82984
+ }
82985
+ return lines2.join(`
82986
+ `);
82987
+ }
82988
+
82989
+ // src/commands/q.ts
82990
+ init_saved_queries();
82991
+ async function qCommand(name, options, command) {
82992
+ try {
82993
+ if (!name?.startsWith("@")) {
82994
+ throw new Error(`Snippet name must start with '@' (got '${name}')`);
82995
+ }
82996
+ const configPath = resolveConfigPath(command, options);
82997
+ const config = await configModule.read(configPath);
82998
+ if (!config.connection)
82999
+ throw new Error('Run "dbcli init" first');
83000
+ const engine = mapSystemToEngine(config.connection.system);
83001
+ if (engine === "mongodb") {
83002
+ throw new Error("Saved queries do not support MongoDB connections");
83003
+ }
83004
+ const dirs = resolveSnippetDirs(process.cwd());
83005
+ const map = await loadSnippets(dirs);
83006
+ const snippet = resolveByName(map, name, engine);
83007
+ const cliParams = parseCliParams(options.param ?? []);
83008
+ const fileParams = await readParamFile(options.paramFile);
83009
+ const prepared = prepareExecution(snippet, { engine, noLimit: options.noLimit === true }, cliParams, fileParams);
83010
+ for (const w of prepared.warnings)
83011
+ console.error(`\u26A0 ${w}`);
83012
+ if (options.dryRun) {
83013
+ console.log("Dry-run preview (no execution):");
83014
+ console.log(prepared.driver.sql);
83015
+ console.log("Bind values: " + JSON.stringify(prepared.driver.values));
83016
+ return;
83017
+ }
83018
+ const adapter = AdapterFactory.createAdapter(config.connection);
83019
+ await adapter.connect();
83020
+ try {
83021
+ const blacklistManager = new BlacklistManager(config);
83022
+ const blacklistValidator = new BlacklistValidator(blacklistManager);
83023
+ const start = performance.now();
83024
+ const result = await adapter.execute(prepared.driver.sql, prepared.driver.values);
83025
+ const executionTimeMs = Math.round(performance.now() - start);
83026
+ const columnNames = result.rows[0] ? Object.keys(result.rows[0]) : [];
83027
+ const filtered = blacklistValidator.filterColumns("", result.rows, columnNames);
83028
+ const formatter = new QueryResultFormatter;
83029
+ const out = formatter.format({
83030
+ rows: filtered.filteredRows,
83031
+ rowCount: filtered.filteredRows.length,
83032
+ columnNames: columnNames.filter((c) => !filtered.omittedColumns.includes(c)),
83033
+ columnTypes: [],
83034
+ executionTimeMs,
83035
+ metadata: {
83036
+ statement: "SELECT",
83037
+ affectedRows: 0,
83038
+ ...filtered.omittedColumns.length > 0 ? {
83039
+ securityNotification: blacklistValidator.buildSecurityNotification("", filtered.omittedColumns)
83040
+ } : {}
83041
+ }
83042
+ }, { format: options.format ?? "table" });
83043
+ console.log(out);
83044
+ } finally {
83045
+ await adapter.disconnect();
83046
+ }
83047
+ } catch (error) {
83048
+ handleQError(error);
83049
+ }
83050
+ }
83051
+ function parseCliParams(list) {
83052
+ const out = {};
83053
+ for (const item of list) {
83054
+ const eq = item.indexOf("=");
83055
+ if (eq === -1)
83056
+ throw new Error(`--param must be key=value (got '${item}')`);
83057
+ out[item.slice(0, eq)] = item.slice(eq + 1);
83058
+ }
83059
+ return out;
83060
+ }
83061
+ async function readParamFile(path) {
83062
+ if (!path)
83063
+ return {};
83064
+ const text2 = await Bun.file(path).text();
83065
+ const parsed = JSON.parse(text2);
83066
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
83067
+ throw new Error(`--param-file must be a JSON object (got ${typeof parsed})`);
83068
+ }
83069
+ return parsed;
83070
+ }
83071
+ function handleQError(error) {
83072
+ if (error instanceof SavedQueryError) {
83073
+ console.error(error.message);
83074
+ process.exit(1);
83075
+ }
83076
+ if (error instanceof BlacklistError) {
83077
+ console.error(error.message);
83078
+ process.exit(1);
83079
+ }
83080
+ if (error instanceof PermissionError) {
83081
+ console.error(t_vars("errors.permission_denied", { required: error.requiredPermission }));
83082
+ process.exit(1);
83083
+ }
83084
+ if (error instanceof ConnectionError) {
83085
+ console.error(t_vars("errors.connection_failed", { message: error.message }));
83086
+ process.exit(1);
83087
+ }
83088
+ console.error(t_vars("errors.message", { message: error.message }));
83089
+ process.exit(1);
83090
+ }
83091
+
83092
+ // src/commands/queries.ts
83093
+ import { mkdir as mkdir4, writeFile as writeFile2 } from "fs/promises";
83094
+ import { dirname as dirname3 } from "path";
83095
+ import { spawn } from "child_process";
83096
+ init_saved_queries();
83097
+ async function deriveEngine() {
83098
+ try {
83099
+ const cfg = await configModule.read(resolveConfigPath(undefined, {}));
83100
+ if (cfg.connection) {
83101
+ const engine = mapSystemToEngine(cfg.connection.system);
83102
+ if (engine !== "mongodb")
83103
+ return engine;
83104
+ }
83105
+ } catch {}
83106
+ return "postgres";
83107
+ }
83108
+ async function queriesList(options) {
83109
+ const map = await loadSnippets(resolveSnippetDirs(process.cwd()));
83110
+ const folded = [...map.entries()].map(([key2, variants]) => foldVariants(key2, variants)).filter((r) => matchesFolded(r, options));
83111
+ if (options.format === "json") {
83112
+ console.log(JSON.stringify(folded, null, 2));
83113
+ return;
83114
+ }
83115
+ if (folded.length === 0) {
83116
+ console.log(t("queries.no_snippets"));
83117
+ return;
83118
+ }
83119
+ const header = ["NAME", "SOURCES", "ENGINES", "PARAMS", "DESCRIPTION"];
83120
+ const cells = folded.map((r) => [
83121
+ r.name + (r.hasLocalOverride ? "*" : ""),
83122
+ r.sources.join(","),
83123
+ r.engines.join(",") || "-",
83124
+ r.params.join(", ") || "-",
83125
+ r.description
83126
+ ]);
83127
+ const widths = header.map((h, i) => Math.max(h.length, ...cells.map((c) => (c[i] ?? "").length)));
83128
+ const fmt = (line) => line.map((c, i) => (c ?? "").padEnd(widths[i] ?? 0)).join(" ");
83129
+ console.log(fmt(header));
83130
+ for (const c of cells)
83131
+ console.log(fmt(c));
83132
+ }
83133
+ var FOLD_SOURCE_RANK = { builtin: 0, shared: 1, local: 2 };
83134
+ function foldVariants(key2, variants) {
83135
+ const sources = unique(variants.map((v) => v.query.source)).sort();
83136
+ const engines = unique(variants.flatMap((v) => v.query.meta.engine ?? [])).sort();
83137
+ const tags = unique(variants.flatMap((v) => v.query.meta.tags ?? []));
83138
+ const top = variants.slice().sort((a, b) => FOLD_SOURCE_RANK[b.query.source] - FOLD_SOURCE_RANK[a.query.source])[0];
83139
+ return {
83140
+ name: key2,
83141
+ sources,
83142
+ engines,
83143
+ params: top.query.meta.params.map((p) => p.name),
83144
+ description: top.query.meta.description ?? "",
83145
+ tags,
83146
+ hasLocalOverride: variants.some((v) => v.hasLocalOverride)
83147
+ };
83148
+ }
83149
+ function unique(xs) {
83150
+ return [...new Set(xs)];
83151
+ }
83152
+ function snippetToJson(s) {
83153
+ return {
83154
+ name: s.query.meta.key,
83155
+ source: s.query.source,
83156
+ engine: s.query.meta.engine?.length === 1 ? s.query.meta.engine[0] : s.query.meta.engine,
83157
+ description: s.query.meta.description,
83158
+ params: s.query.meta.params.map((p) => ({
83159
+ name: p.name,
83160
+ type: p.type,
83161
+ ...p.default !== undefined ? { default: p.default } : {},
83162
+ ...p.required ? { required: true } : {},
83163
+ ...p.description ? { description: p.description } : {},
83164
+ ...p.enum ? { enum: p.enum } : {}
83165
+ })),
83166
+ tags: s.query.meta.tags,
83167
+ file: s.query.file,
83168
+ hasLocalOverride: s.hasLocalOverride || undefined
83169
+ };
83170
+ }
83171
+ function matchesFolded(r, opts) {
83172
+ if (opts.tag && !r.tags.includes(opts.tag))
83173
+ return false;
83174
+ if (opts.engine) {
83175
+ if (r.engines.length > 0 && !r.engines.includes(opts.engine))
83176
+ return false;
83177
+ }
83178
+ if (opts.source && !r.sources.includes(opts.source))
83179
+ return false;
83180
+ return true;
83181
+ }
83182
+ async function queriesShow(name, options) {
83183
+ const map = await loadSnippets(resolveSnippetDirs(process.cwd()));
83184
+ try {
83185
+ const engine = await deriveEngine();
83186
+ const snippet = resolveByName(map, name, engine);
83187
+ if (options.format === "json") {
83188
+ console.log(JSON.stringify({ ...snippetToJson(snippet), sql: snippet.query.sqlBody.trim() }, null, 2));
83189
+ return;
83190
+ }
83191
+ console.log(`# ${snippet.query.meta.name} (${snippet.query.source})`);
83192
+ if (snippet.query.meta.description)
83193
+ console.log(`# ${snippet.query.meta.description}`);
83194
+ if (snippet.query.meta.engine)
83195
+ console.log(`# engine: ${snippet.query.meta.engine.join(",")}`);
83196
+ if (snippet.query.meta.params.length > 0) {
83197
+ console.log("# params:");
83198
+ for (const p of snippet.query.meta.params) {
83199
+ const def = p.default !== undefined ? ` (default: ${p.default})` : "";
83200
+ const req = p.required ? " (required)" : "";
83201
+ console.log(`# ${p.name}: ${p.type}${req}${def}`);
83202
+ }
83203
+ }
83204
+ console.log("");
83205
+ console.log(snippet.query.sqlBody.trim());
83206
+ } catch (e) {
83207
+ console.error(e.message);
83208
+ process.exit(1);
83209
+ }
83210
+ }
83211
+ async function queriesNew(name, options) {
83212
+ if (!name.startsWith("@")) {
83213
+ console.error(`Snippet name must start with '@' (got '${name}')`);
83214
+ process.exit(1);
83215
+ return;
83216
+ }
83217
+ const source = options.local ? "local" : "shared";
83218
+ const file = snippetKeyToFile(process.cwd(), name, source);
83219
+ if (await Bun.file(file).exists()) {
83220
+ console.error(`Snippet already exists: ${file}`);
83221
+ process.exit(1);
83222
+ return;
83223
+ }
83224
+ await mkdir4(dirname3(file), { recursive: true });
83225
+ await writeFile2(file, scaffold(name), "utf8");
83226
+ console.log(`Created ${file}`);
83227
+ if (source === "shared")
83228
+ console.log(t("queries.first_run_hint"));
83229
+ if (options.edit)
83230
+ await openInEditor(file);
83231
+ }
83232
+ async function queriesEdit(name, options) {
83233
+ const candidates = options.shared ? [snippetKeyToFile(process.cwd(), name, "shared")] : [
83234
+ snippetKeyToFile(process.cwd(), name, "local"),
83235
+ snippetKeyToFile(process.cwd(), name, "shared")
83236
+ ];
83237
+ for (const f of candidates) {
83238
+ if (await Bun.file(f).exists()) {
83239
+ await openInEditor(f);
83240
+ return;
83241
+ }
83242
+ }
83243
+ console.error(`Snippet not found locally or in shared: ${name}`);
83244
+ process.exit(1);
83245
+ }
83246
+ async function queriesCheck(options) {
83247
+ const dirs = resolveSnippetDirs(process.cwd());
83248
+ let failed = 0;
83249
+ let total = 0;
83250
+ try {
83251
+ const map = await loadSnippets(dirs);
83252
+ for (const variants of map.values()) {
83253
+ for (const snippet of variants) {
83254
+ total++;
83255
+ try {
83256
+ const text2 = await Bun.file(snippet.query.file).text();
83257
+ const result = parseSavedQuery({
83258
+ key: snippet.query.meta.key,
83259
+ file: snippet.query.file,
83260
+ source: snippet.query.source,
83261
+ text: text2
83262
+ });
83263
+ if (options.strict && result.warnings.length > 0) {
83264
+ console.error(`\u2717 ${snippet.query.meta.key}: ${result.warnings.join("; ")}`);
83265
+ failed++;
83266
+ }
83267
+ } catch (e) {
83268
+ const msg = e instanceof SavedQueryError ? e.message : e.message;
83269
+ console.error(`\u2717 ${snippet.query.meta.key}: ${msg}`);
83270
+ failed++;
83271
+ }
83272
+ }
83273
+ }
83274
+ } catch (e) {
83275
+ const msg = e instanceof SavedQueryError ? e.message : e.message;
83276
+ console.error(`\u2717 ${msg}`);
83277
+ failed++;
83278
+ }
83279
+ if (failed > 0) {
83280
+ console.error(`\u2717 ${failed} snippet(s) have errors`);
83281
+ process.exit(1);
83282
+ } else {
83283
+ console.log(`\u2713 ${total} snippets parsed successfully`);
83284
+ }
83285
+ }
83286
+ function scaffold(name) {
83287
+ return [
83288
+ "-- ---",
83289
+ `-- name: ${name.replace(/^@/, "")}`,
83290
+ "-- description: TODO",
83291
+ "-- engine: postgres",
83292
+ "-- params: {}",
83293
+ "-- tags: []",
83294
+ "-- ---",
83295
+ "",
83296
+ "SELECT 1;",
83297
+ ""
83298
+ ].join(`
83299
+ `);
83300
+ }
83301
+ async function openInEditor(file) {
83302
+ const editor = process.env.EDITOR || "vi";
83303
+ await new Promise((resolve3, reject) => {
83304
+ const child = spawn(editor, [file], { stdio: "inherit" });
83305
+ child.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Editor exited with ${code}`)));
83306
+ });
83307
+ }
83308
+ var queriesCommand = new Command("queries").description(t("queries.description"));
83309
+ queriesCommand.command("list").description(t("queries.list_description")).option("--format <type>", "Output format: table, json, csv", "table").option("--tag <tag>", "Filter by tag").option("--engine <engine>", "Filter by engine: postgres | mysql").option("--source <source>", "Filter by source: local | shared").action(async (options) => {
83310
+ await queriesList(options);
83311
+ });
83312
+ queriesCommand.command("show <name>").description(t("queries.show_description")).option("--format <type>", "Output format: table, json, csv", "table").action(async (name, options) => {
83313
+ await queriesShow(name, options);
83314
+ });
83315
+ queriesCommand.command("new <name>").description(t("queries.new_description")).option("--local", "Create under .dbcli/queries (gitignored) instead of .dbcli-shared/").option("--edit", "Open the created file in $EDITOR").action(async (name, options) => {
83316
+ await queriesNew(name, options);
83317
+ });
83318
+ queriesCommand.command("edit <name>").description(t("queries.edit_description")).option("--shared", "Edit the shared file instead of local").action(async (name, options) => {
83319
+ await queriesEdit(name, options);
83320
+ });
83321
+ queriesCommand.command("check").description(t("queries.check_description")).option("--strict", "Promote warnings to errors").option("--format <type>", "Output format: table, json, csv", "table").action(async (options) => {
83322
+ await queriesCheck(options);
83323
+ });
83324
+ queriesCommand.command("delete <name>").description(t("queries.delete_description")).option("--force", "Skip confirmation prompt").action(async (name, options) => {
83325
+ try {
83326
+ const { queriesDelete: queriesDelete2 } = await Promise.resolve().then(() => (init_queries_delete(), exports_queries_delete));
83327
+ await queriesDelete2(name, options);
83328
+ } catch (e) {
83329
+ console.error(e.message);
83330
+ process.exit(1);
83331
+ }
83332
+ });
83333
+ queriesCommand.command("rename <old> <new>").description(t("queries.rename_description")).option("--force", "Skip confirmation prompt").action(async (oldName, newName, options) => {
83334
+ try {
83335
+ const { queriesRename: queriesRename2 } = await Promise.resolve().then(() => (init_queries_rename(), exports_queries_rename));
83336
+ await queriesRename2(oldName, newName, options);
83337
+ } catch (e) {
83338
+ console.error(e.message);
83339
+ process.exit(1);
83340
+ }
83341
+ });
83342
+ queriesCommand.command("copy <src> <dst>").description(t("queries.copy_description")).action(async (src, dst) => {
83343
+ try {
83344
+ const { queriesCopy: queriesCopy2 } = await Promise.resolve().then(() => (init_queries_copy(), exports_queries_copy));
83345
+ await queriesCopy2(src, dst);
83346
+ } catch (e) {
83347
+ console.error(e.message);
83348
+ process.exit(1);
83349
+ }
83350
+ });
83351
+ queriesCommand.command("import <path>").description(t("queries.import_description")).option("--force", "Overwrite existing file without prompting").option("--as <name>", "Override snippet name (defaults to filename)").action(async (path, options) => {
83352
+ try {
83353
+ const { queriesImport: queriesImport2 } = await Promise.resolve().then(() => (init_queries_import(), exports_queries_import));
83354
+ await queriesImport2(path, options);
83355
+ } catch (e) {
83356
+ console.error(e.message);
83357
+ process.exit(1);
83358
+ }
83359
+ });
83360
+ queriesCommand.command("export <name>").description(t("queries.export_description")).option("--output <path>", "Write to a file instead of stdout").option("--engine <engine>", "Pick a specific variant: postgres | mysql").action(async (name, options) => {
83361
+ try {
83362
+ const { queriesExport: queriesExport2 } = await Promise.resolve().then(() => (init_queries_export(), exports_queries_export));
83363
+ await queriesExport2(name, options);
83364
+ } catch (e) {
83365
+ console.error(e.message);
83366
+ process.exit(1);
83367
+ }
83368
+ });
83369
+
81704
83370
  // src/core/data-executor.ts
81705
83371
  class DataExecutor {
81706
83372
  adapter;
@@ -82568,11 +84234,11 @@ async function writeSkillInstall(platform, installPath, skillMarkdown, reference
82568
84234
  async function ensureDir(dirPath) {
82569
84235
  try {
82570
84236
  await $`mkdir -p ${dirPath}`.quiet();
82571
- } catch (error) {
84237
+ } catch {
82572
84238
  try {
82573
- const { mkdir } = await import("fs/promises");
82574
- await mkdir(dirPath, { recursive: true });
82575
- } catch (fsError) {
84239
+ const { mkdir: mkdir5 } = await import("fs/promises");
84240
+ await mkdir5(dirPath, { recursive: true });
84241
+ } catch {
82576
84242
  throw new Error(`Cannot create directory: ${dirPath}`);
82577
84243
  }
82578
84244
  }
@@ -82657,7 +84323,7 @@ async function blacklistTableRemove(tableName, configPath) {
82657
84323
  }
82658
84324
  const newBlacklist = {
82659
84325
  ...blacklist,
82660
- tables: blacklist.tables.filter((t6) => t6 !== tableName)
84326
+ tables: blacklist.tables.filter((t2) => t2 !== tableName)
82661
84327
  };
82662
84328
  await configModule.write(configPath, { ...config, blacklist: newBlacklist });
82663
84329
  console.log(t_vars("blacklist.table_removed", { table: tableName }));
@@ -82757,11 +84423,11 @@ columnCmd.command("remove <table.column>").description("Remove column from black
82757
84423
  // src/commands/check.ts
82758
84424
  init_size_category();
82759
84425
  init_validation();
82760
- var ALLOWED_FORMATS4 = ["json", "table"];
84426
+ var ALLOWED_FORMATS5 = ["json", "table"];
82761
84427
  var checkCommand = new Command().name("check").description("Run data health checks on tables").argument("[table]", "Table to check (omit for --all)").option("--all", "Check all tables (skips huge tables unless --include-large)", false).option("--include-large", "Include huge tables in --all scan", false).option("--checks <types>", "Comma-separated checks: nulls,duplicates,orphans,emptyStrings", undefined).option("--sample <number>", "Sample size for large tables (default: 10000)", "10000").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(checkAction);
82762
84428
  async function checkAction(table, options) {
82763
84429
  try {
82764
- validateFormat(options.format, ALLOWED_FORMATS4, "check");
84430
+ validateFormat(options.format, ALLOWED_FORMATS5, "check");
82765
84431
  const config = await configModule.read(options.config);
82766
84432
  if (!config.connection) {
82767
84433
  console.error("Database not configured. Run: dbcli init");
@@ -82792,21 +84458,21 @@ async function checkAction(table, options) {
82792
84458
  const tables = await adapter.listTables();
82793
84459
  const reports = [];
82794
84460
  const skipped = [];
82795
- for (const t6 of tables) {
82796
- if (blacklistedTables.has(t6.name.toLowerCase())) {
82797
- skipped.push(`${t6.name} (blacklisted)`);
84461
+ for (const t2 of tables) {
84462
+ if (blacklistedTables.has(t2.name.toLowerCase())) {
84463
+ skipped.push(`${t2.name} (blacklisted)`);
82798
84464
  continue;
82799
84465
  }
82800
- if (t6.tableType === "view") {
82801
- skipped.push(`${t6.name} (view)`);
84466
+ if (t2.tableType === "view") {
84467
+ skipped.push(`${t2.name} (view)`);
82802
84468
  continue;
82803
84469
  }
82804
- const category = getSizeCategory(t6.estimatedRowCount);
84470
+ const category = getSizeCategory(t2.estimatedRowCount);
82805
84471
  if (category === "huge" && !options.includeLarge) {
82806
- skipped.push(`${t6.name} (~${(t6.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
84472
+ skipped.push(`${t2.name} (~${(t2.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
82807
84473
  continue;
82808
84474
  }
82809
- const schema = await adapter.getTableSchema(t6.name);
84475
+ const schema = await adapter.getTableSchema(t2.name);
82810
84476
  const report = await checker.check(schema, {
82811
84477
  checks: checkTypes,
82812
84478
  sample: sampleSize,
@@ -82886,12 +84552,12 @@ function getBlacklistedTableSet(manager) {
82886
84552
 
82887
84553
  // src/commands/diff.ts
82888
84554
  init_validation();
82889
- var ALLOWED_FORMATS5 = ["json", "table"];
84555
+ var ALLOWED_FORMATS6 = ["json", "table"];
82890
84556
  function compareSnapshots(before, after) {
82891
84557
  const beforeTables = new Set(Object.keys(before.tables));
82892
84558
  const afterTables = new Set(Object.keys(after.tables));
82893
- const addedTables = Array.from(afterTables).filter((t6) => !beforeTables.has(t6));
82894
- const removedTables = Array.from(beforeTables).filter((t6) => !afterTables.has(t6));
84559
+ const addedTables = Array.from(afterTables).filter((t2) => !beforeTables.has(t2));
84560
+ const removedTables = Array.from(beforeTables).filter((t2) => !afterTables.has(t2));
82895
84561
  const addedColumns = [];
82896
84562
  const removedColumns = [];
82897
84563
  const modifiedColumns = [];
@@ -82917,7 +84583,7 @@ function compareSnapshots(before, after) {
82917
84583
  removedColumns.push({ table: tableName, column: col.name, type: col.type });
82918
84584
  }
82919
84585
  }
82920
- const commonTables = Array.from(afterTables).filter((t6) => beforeTables.has(t6));
84586
+ const commonTables = Array.from(afterTables).filter((t2) => beforeTables.has(t2));
82921
84587
  for (const tableName of commonTables) {
82922
84588
  const beforeTable = before.tables[tableName];
82923
84589
  const afterTable = after.tables[tableName];
@@ -82980,7 +84646,7 @@ function compareSnapshots(before, after) {
82980
84646
  var diffCommand = new Command().name("diff").description("Compare schema snapshots to detect changes").option("--snapshot <path>", "Save current schema snapshot to file").option("--against <path>", "Compare current schema against a snapshot file").option("--format <format>", "Output format: json (default) or table", "json").option("--config <path>", "Path to .dbcli config file", ".dbcli").action(diffAction);
82981
84647
  async function diffAction(options) {
82982
84648
  try {
82983
- validateFormat(options.format, ALLOWED_FORMATS5, "diff");
84649
+ validateFormat(options.format, ALLOWED_FORMATS6, "diff");
82984
84650
  if (!options.snapshot && !options.against) {
82985
84651
  console.error("Specify --snapshot <path> to save, or --against <path> to compare");
82986
84652
  process.exit(1);
@@ -83002,11 +84668,11 @@ async function diffAction(options) {
83002
84668
  tables: {},
83003
84669
  createdAt: new Date().toISOString()
83004
84670
  };
83005
- for (const t6 of tables) {
83006
- if (t6.tableType === "view")
84671
+ for (const t2 of tables) {
84672
+ if (t2.tableType === "view")
83007
84673
  continue;
83008
- const schema = await adapter.getTableSchema(t6.name);
83009
- currentSnapshot.tables[t6.name] = {
84674
+ const schema = await adapter.getTableSchema(t2.name);
84675
+ currentSnapshot.tables[t2.name] = {
83010
84676
  name: schema.name,
83011
84677
  columns: schema.columns,
83012
84678
  indexes: schema.indexes || []
@@ -83080,10 +84746,10 @@ Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
83080
84746
 
83081
84747
  // src/commands/status.ts
83082
84748
  init_validation();
83083
- var ALLOWED_FORMATS6 = ["text", "json"];
84749
+ var ALLOWED_FORMATS7 = ["text", "json"];
83084
84750
  var statusCommand = new Command("status").description("Show current configuration status (safe for AI agents, no credentials exposed)").option("--format <type>", "Output format: text, json", "json").action(async (options) => {
83085
84751
  try {
83086
- validateFormat(options.format, ALLOWED_FORMATS6, "status");
84752
+ validateFormat(options.format, ALLOWED_FORMATS7, "status");
83087
84753
  const configPath = resolveConfigPath(statusCommand);
83088
84754
  const config = await configModule.read(configPath);
83089
84755
  if (!config.connection) {
@@ -83119,9 +84785,9 @@ var statusCommand = new Command("status").description("Show current configuratio
83119
84785
  // src/commands/doctor.ts
83120
84786
  init_validation();
83121
84787
  init_schema_path();
83122
- import { join as join11 } from "path";
84788
+ import { join as join14 } from "path";
83123
84789
  import { resolveSrv as resolveSrv2 } from "dns/promises";
83124
- var ALLOWED_FORMATS7 = ["text", "json"];
84790
+ var ALLOWED_FORMATS8 = ["text", "json"];
83125
84791
  var SENSITIVE_PATTERNS = [
83126
84792
  "password",
83127
84793
  "passwd",
@@ -83193,7 +84859,7 @@ var runDoctorChecks = {
83193
84859
  }
83194
84860
  },
83195
84861
  async checkConfigExists(configPath, existsFn) {
83196
- const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join11(configPath, "config.json")).exists();
84862
+ const exists = existsFn ? await existsFn(configPath) : await Bun.file(configPath).exists() || await Bun.file(join14(configPath, "config.json")).exists();
83197
84863
  return {
83198
84864
  group: "Configuration",
83199
84865
  label: "Config exists",
@@ -83330,7 +84996,7 @@ var runDoctorChecks = {
83330
84996
  }
83331
84997
  },
83332
84998
  checkLargeTables(tables) {
83333
- const large = tables.filter((t6) => (t6.estimatedRowCount ?? 0) > 1e6);
84999
+ const large = tables.filter((t2) => (t2.estimatedRowCount ?? 0) > 1e6);
83334
85000
  if (large.length === 0) {
83335
85001
  return {
83336
85002
  group: "Connection & Data",
@@ -83339,7 +85005,7 @@ var runDoctorChecks = {
83339
85005
  message: "No tables exceed 1M rows"
83340
85006
  };
83341
85007
  }
83342
- const list = large.map((t6) => `${t6.name} (${((t6.estimatedRowCount ?? 0) / 1e6).toFixed(1)}M rows)`).join(", ");
85008
+ const list = large.map((t2) => `${t2.name} (${((t2.estimatedRowCount ?? 0) / 1e6).toFixed(1)}M rows)`).join(", ");
83343
85009
  return {
83344
85010
  group: "Connection & Data",
83345
85011
  label: "Large tables",
@@ -83350,7 +85016,7 @@ var runDoctorChecks = {
83350
85016
  async checkV2Config(configPath) {
83351
85017
  const results = [];
83352
85018
  const storagePath = await resolveConfigStoragePath(configPath);
83353
- const configFile = Bun.file(join11(storagePath, "config.json"));
85019
+ const configFile = Bun.file(join14(storagePath, "config.json"));
83354
85020
  if (!await configFile.exists())
83355
85021
  return results;
83356
85022
  let raw;
@@ -83390,7 +85056,7 @@ var runDoctorChecks = {
83390
85056
  }
83391
85057
  for (const [name, conn] of Object.entries(config.connections)) {
83392
85058
  if (conn.envFile) {
83393
- const envPath = join11(storagePath, conn.envFile);
85059
+ const envPath = join14(storagePath, conn.envFile);
83394
85060
  const exists = await Bun.file(envPath).exists();
83395
85061
  results.push({
83396
85062
  group: "Configuration",
@@ -83453,10 +85119,12 @@ async function collectMongoDoctorResults(config) {
83453
85119
  message: `MongoDB ${version}`
83454
85120
  });
83455
85121
  } catch {}
83456
- const collections = await adapter.listTables();
85122
+ const collections = adapter.listTables ? await adapter.listTables() : [];
83457
85123
  const tableColumns = new Map;
83458
85124
  for (const coll of collections) {
83459
85125
  try {
85126
+ if (!adapter.getTableSchema)
85127
+ continue;
83460
85128
  const schema = await adapter.getTableSchema(coll.name);
83461
85129
  tableColumns.set(coll.name, schema.columns.map((c) => c.name));
83462
85130
  } catch {}
@@ -83493,7 +85161,7 @@ async function collectMongoDoctorResults(config) {
83493
85161
  return results;
83494
85162
  }
83495
85163
  var doctorCommand = new Command("doctor").description("Run diagnostic checks on dbcli configuration, environment, and connection").option("--format <type>", "Output format: text, json", "text").action(async (options) => {
83496
- validateFormat(options.format, ALLOWED_FORMATS7, "doctor");
85164
+ validateFormat(options.format, ALLOWED_FORMATS8, "doctor");
83497
85165
  const logger = getLogger();
83498
85166
  const results = [];
83499
85167
  const configPath = resolveConfigPath(doctorCommand);
@@ -83552,8 +85220,8 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
83552
85220
  try {
83553
85221
  const tables = await adapter.listTables();
83554
85222
  const tableColumns = new Map;
83555
- for (const t6 of tables) {
83556
- tableColumns.set(t6.name, t6.columns.map((c) => c.name));
85223
+ for (const t2 of tables) {
85224
+ tableColumns.set(t2.name, t2.columns.map((c) => c.name));
83557
85225
  }
83558
85226
  results.push(runDoctorChecks.checkBlacklistCompleteness(tableColumns, blacklistedColumns));
83559
85227
  results.push(runDoctorChecks.checkLargeTables(tables));
@@ -83562,7 +85230,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
83562
85230
  }
83563
85231
  try {
83564
85232
  const schemaConnName = await getSchemaIsolationConnectionName(configPath);
83565
- const indexPath = join11(resolveSchemaPath(storagePath, schemaConnName), "index.json");
85233
+ const indexPath = join14(resolveSchemaPath(storagePath, schemaConnName), "index.json");
83566
85234
  const indexFile = Bun.file(indexPath);
83567
85235
  let indexParsed = null;
83568
85236
  if (await indexFile.exists()) {
@@ -83604,7 +85272,7 @@ var doctorCommand = new Command("doctor").description("Run diagnostic checks on
83604
85272
  });
83605
85273
 
83606
85274
  // src/commands/completion.ts
83607
- import { join as join12 } from "path";
85275
+ import { join as join15 } from "path";
83608
85276
  import { homedir as homedir3 } from "os";
83609
85277
  function extractCommands(program2) {
83610
85278
  return program2.commands.map((cmd) => ({
@@ -83706,11 +85374,11 @@ function getInstallPath2(shell) {
83706
85374
  const home = homedir3();
83707
85375
  switch (shell) {
83708
85376
  case "bash":
83709
- return join12(home, ".bashrc");
85377
+ return join15(home, ".bashrc");
83710
85378
  case "zsh":
83711
- return join12(home, ".zshrc");
85379
+ return join15(home, ".zshrc");
83712
85380
  case "fish":
83713
- return join12(home, ".config", "fish", "completions", "dbcli.fish");
85381
+ return join15(home, ".config", "fish", "completions", "dbcli.fish");
83714
85382
  default:
83715
85383
  throw new Error(`Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
83716
85384
  }
@@ -83730,7 +85398,7 @@ var MARKER_END = "# <<< dbcli completion <<<";
83730
85398
  async function installCompletion(shell, script) {
83731
85399
  const targetPath = getInstallPath2(shell);
83732
85400
  if (shell === "fish") {
83733
- const dir = join12(homedir3(), ".config", "fish", "completions");
85401
+ const dir = join15(homedir3(), ".config", "fish", "completions");
83734
85402
  await Bun.$`mkdir -p ${dir}`.quiet();
83735
85403
  await Bun.file(targetPath).write(script);
83736
85404
  console.log(colors.success(`\u2713 Fish completion installed to ${targetPath}`));
@@ -83957,7 +85625,7 @@ ${t("upgrade.failed")}`));
83957
85625
 
83958
85626
  // src/commands/shell.ts
83959
85627
  import { createInterface as createInterface2 } from "readline";
83960
- import { join as join13 } from "path";
85628
+ import { join as join16 } from "path";
83961
85629
  import { homedir as homedir4 } from "os";
83962
85630
 
83963
85631
  // src/core/repl/types.ts
@@ -84083,7 +85751,6 @@ var META_COMMANDS = [
84083
85751
  var META_PREFIX = ".";
84084
85752
  var SQL_TERMINATOR = ";";
84085
85753
  var sqlKeywordSet = new Set(SQL_KEYWORDS_FOR_DETECTION);
84086
- var dbcliCommandSet = new Set(DBCLI_COMMANDS);
84087
85754
  var metaCommandNames = META_COMMANDS.map((m) => m.slice(1));
84088
85755
  function classifyInput(raw) {
84089
85756
  const normalized = raw.trim();
@@ -84091,12 +85758,12 @@ function classifyInput(raw) {
84091
85758
  return { type: "empty", raw, normalized };
84092
85759
  }
84093
85760
  if (normalized.startsWith(META_PREFIX)) {
84094
- const cmdName = normalized.split(/\s+/)[0].slice(1);
85761
+ const cmdName = (normalized.split(/\s+/)[0] ?? "").slice(1);
84095
85762
  if (metaCommandNames.includes(cmdName)) {
84096
85763
  return { type: "meta", raw, normalized };
84097
85764
  }
84098
85765
  }
84099
- const firstWord = normalized.split(/\s+/)[0].toUpperCase();
85766
+ const firstWord = (normalized.split(/\s+/)[0] ?? "").toUpperCase();
84100
85767
  if (sqlKeywordSet.has(firstWord)) {
84101
85768
  return { type: "sql", raw, normalized };
84102
85769
  }
@@ -84303,7 +85970,7 @@ function isKnownCommand(name) {
84303
85970
 
84304
85971
  // src/core/repl/history-manager.ts
84305
85972
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
84306
- import { dirname as dirname2 } from "path";
85973
+ import { dirname as dirname5 } from "path";
84307
85974
  var DEFAULT_MAX = 1000;
84308
85975
 
84309
85976
  class HistoryManager {
@@ -84331,7 +85998,7 @@ class HistoryManager {
84331
85998
  return this.entries;
84332
85999
  }
84333
86000
  async save() {
84334
- const dir = dirname2(this.filePath);
86001
+ const dir = dirname5(this.filePath);
84335
86002
  if (!existsSync(dir)) {
84336
86003
  mkdirSync(dir, { recursive: true });
84337
86004
  }
@@ -84478,7 +86145,7 @@ class ReplEngine {
84478
86145
  const tableName = this.extractTableName(sql);
84479
86146
  if (tableName) {
84480
86147
  const blacklistedTables = this.config.blacklist.tables ?? [];
84481
- const isBlacklisted = blacklistedTables.some((t6) => t6.toLowerCase() === tableName.toLowerCase());
86148
+ const isBlacklisted = blacklistedTables.some((t2) => t2.toLowerCase() === tableName.toLowerCase());
84482
86149
  if (isBlacklisted) {
84483
86150
  return {
84484
86151
  action: "continue",
@@ -84489,9 +86156,10 @@ class ReplEngine {
84489
86156
  }
84490
86157
  const startTime = Date.now();
84491
86158
  try {
84492
- const rows = await this.adapter.execute(sql);
86159
+ const result = await this.adapter.execute(sql);
84493
86160
  const elapsed = Date.now() - startTime;
84494
- const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
86161
+ const rows = result.rows;
86162
+ const columnNames = rows.length > 0 && rows[0] ? Object.keys(rows[0]) : [];
84495
86163
  const queryResult = {
84496
86164
  rows,
84497
86165
  rowCount: rows.length,
@@ -84535,8 +86203,9 @@ class ReplEngine {
84535
86203
  return match?.[1];
84536
86204
  }
84537
86205
  isConnectionError(error) {
84538
- const msg = (error.message ?? "").toLowerCase();
84539
- return error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "ETIMEDOUT" || msg.includes("connection") || msg.includes("terminated") || msg.includes("socket");
86206
+ const e = error;
86207
+ const msg = (e.message ?? "").toLowerCase();
86208
+ return e.code === "ECONNREFUSED" || e.code === "ECONNRESET" || e.code === "ETIMEDOUT" || msg.includes("connection") || msg.includes("terminated") || msg.includes("socket");
84540
86209
  }
84541
86210
  }
84542
86211
 
@@ -84592,15 +86261,16 @@ function createCompleter(ctx) {
84592
86261
  return [[...cmdHits[0], ...sqlHits[0]], lastWord];
84593
86262
  }
84594
86263
  const cmdName = words[0];
84595
- if (COMMANDS_TAKING_TABLE_ARG.has(cmdName)) {
86264
+ if (cmdName && COMMANDS_TAKING_TABLE_ARG.has(cmdName)) {
84596
86265
  return matchWithSuffix(allTableNames, lastWord);
84597
86266
  }
84598
86267
  return matchWithSuffix(allTableNames, lastWord);
84599
86268
  };
84600
86269
  }
84601
86270
  function completeMetaCommands(line) {
84602
- const hits = META_COMMANDS.filter((m) => m.startsWith(line.split(/\s+/)[0].toLowerCase())).map((m) => m + " ");
84603
- return [hits, line.split(/\s+/)[0]];
86271
+ const first = line.split(/\s+/)[0] ?? "";
86272
+ const hits = META_COMMANDS.filter((m) => m.startsWith(first.toLowerCase())).map((m) => m + " ");
86273
+ return [hits, first];
84604
86274
  }
84605
86275
  function getLastWord(line) {
84606
86276
  const parts = line.split(/\s+/);
@@ -84615,7 +86285,7 @@ function getPreviousWord(line) {
84615
86285
  if (line.endsWith(" ") || line.endsWith("\t")) {
84616
86286
  return parts[parts.length - 1] || "";
84617
86287
  }
84618
- return parts.length >= 2 ? parts[parts.length - 2] : "";
86288
+ return parts.length >= 2 ? parts[parts.length - 2] ?? "" : "";
84619
86289
  }
84620
86290
  function isColumnPosition(upperLine) {
84621
86291
  const columnKeywords = ["WHERE ", "AND ", "OR ", "ON ", "SET ", "SELECT "];
@@ -84625,9 +86295,9 @@ function extractTableFromLine(line, tableNames) {
84625
86295
  const upper = line.toUpperCase();
84626
86296
  const fromIdx = upper.lastIndexOf("FROM ");
84627
86297
  if (fromIdx >= 0) {
84628
- const afterFrom = line.slice(fromIdx + 5).trim().split(/\s+/)[0];
86298
+ const afterFrom = line.slice(fromIdx + 5).trim().split(/\s+/)[0] ?? "";
84629
86299
  const candidate = afterFrom.replace(/[;,]/g, "").toLowerCase();
84630
- return tableNames.find((t6) => t6.toLowerCase() === candidate);
86300
+ return tableNames.find((t2) => t2.toLowerCase() === candidate);
84631
86301
  }
84632
86302
  return;
84633
86303
  }
@@ -84652,7 +86322,7 @@ class MongoShellAdapter {
84652
86322
  async disconnect() {
84653
86323
  await this.adapter.disconnect();
84654
86324
  }
84655
- async execute() {
86325
+ async execute(_sql, _params) {
84656
86326
  throw new Error("MongoDB shell does not support raw SQL. Use `query <json>` with `--collection`.");
84657
86327
  }
84658
86328
  async listTables() {
@@ -84677,7 +86347,7 @@ class MongoShellAdapter {
84677
86347
  }
84678
86348
 
84679
86349
  // src/commands/shell.ts
84680
- var HISTORY_PATH = join13(homedir4(), ".dbcli_history");
86350
+ var HISTORY_PATH = join16(homedir4(), ".dbcli_history");
84681
86351
  var shellCommand = new Command("shell").description("Interactive database shell with auto-completion and syntax highlighting").option("--sql", "SQL-only mode (skip dbcli command parsing)").action(async (options, command) => {
84682
86352
  const configPath = resolveConfigPath(command);
84683
86353
  await runShell(options, configPath);
@@ -84870,25 +86540,25 @@ class PostgreSQLDDLGenerator {
84870
86540
  alterColumn(options) {
84871
86541
  const statements = [];
84872
86542
  const warnings = [];
84873
- const t6 = q(options.table);
86543
+ const t2 = q(options.table);
84874
86544
  const c = q(options.column);
84875
86545
  if (options.type) {
84876
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} TYPE ${options.type.toUpperCase()};`);
86546
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} TYPE ${options.type.toUpperCase()};`);
84877
86547
  }
84878
86548
  if (options.rename) {
84879
- statements.push(`ALTER TABLE ${t6} RENAME COLUMN ${c} TO ${q(options.rename)};`);
86549
+ statements.push(`ALTER TABLE ${t2} RENAME COLUMN ${c} TO ${q(options.rename)};`);
84880
86550
  }
84881
86551
  if (options.setDefault !== undefined) {
84882
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} SET DEFAULT ${options.setDefault};`);
86552
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} SET DEFAULT ${options.setDefault};`);
84883
86553
  }
84884
86554
  if (options.dropDefault) {
84885
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} DROP DEFAULT;`);
86555
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} DROP DEFAULT;`);
84886
86556
  }
84887
86557
  if (options.setNullable) {
84888
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} DROP NOT NULL;`);
86558
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} DROP NOT NULL;`);
84889
86559
  }
84890
86560
  if (options.dropNullable) {
84891
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} SET NOT NULL;`);
86561
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} SET NOT NULL;`);
84892
86562
  }
84893
86563
  if (statements.length === 0) {
84894
86564
  warnings.push("No alter operations specified");
@@ -84897,12 +86567,12 @@ class PostgreSQLDDLGenerator {
84897
86567
  `), warnings };
84898
86568
  }
84899
86569
  addIndex(index) {
84900
- const unique = index.unique ? "UNIQUE " : "";
86570
+ const unique2 = index.unique ? "UNIQUE " : "";
84901
86571
  const name = index.name || `idx_${index.table}_${index.columns.join("_")}`;
84902
86572
  const using = index.type ? ` USING ${index.type.toUpperCase()}` : "";
84903
86573
  const cols = index.columns.map(q).join(", ");
84904
86574
  return {
84905
- sql: `CREATE ${unique}INDEX ${q(name)} ON ${q(index.table)}${using} (${cols});`,
86575
+ sql: `CREATE ${unique2}INDEX ${q(name)} ON ${q(index.table)}${using} (${cols});`,
84906
86576
  warnings: []
84907
86577
  };
84908
86578
  }
@@ -84910,14 +86580,14 @@ class PostgreSQLDDLGenerator {
84910
86580
  return { sql: `DROP INDEX ${q(indexName)};`, warnings: [] };
84911
86581
  }
84912
86582
  addConstraint(constraint) {
84913
- const t6 = q(constraint.table);
86583
+ const t2 = q(constraint.table);
84914
86584
  const warnings = [];
84915
86585
  switch (constraint.type) {
84916
86586
  case "foreign_key": {
84917
86587
  const name = constraint.name || `fk_${constraint.table}_${constraint.column}`;
84918
86588
  const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
84919
86589
  return {
84920
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q(name)} FOREIGN KEY (${q(constraint.column)}) REFERENCES ${q(constraint.references.table)}(${q(constraint.references.column)})${onDelete};`,
86590
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name)} FOREIGN KEY (${q(constraint.column)}) REFERENCES ${q(constraint.references.table)}(${q(constraint.references.column)})${onDelete};`,
84921
86591
  warnings
84922
86592
  };
84923
86593
  }
@@ -84925,14 +86595,14 @@ class PostgreSQLDDLGenerator {
84925
86595
  const name = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
84926
86596
  const cols = constraint.columns.map(q).join(", ");
84927
86597
  return {
84928
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q(name)} UNIQUE (${cols});`,
86598
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name)} UNIQUE (${cols});`,
84929
86599
  warnings
84930
86600
  };
84931
86601
  }
84932
86602
  case "check": {
84933
86603
  const name = constraint.name || `ck_${constraint.table}`;
84934
86604
  return {
84935
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q(name)} CHECK (${constraint.expression});`,
86605
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name)} CHECK (${constraint.expression});`,
84936
86606
  warnings
84937
86607
  };
84938
86608
  }
@@ -85035,19 +86705,19 @@ class MySQLDDLGenerator {
85035
86705
  alterColumn(options) {
85036
86706
  const statements = [];
85037
86707
  const warnings = [];
85038
- const t6 = q2(options.table);
86708
+ const t2 = q2(options.table);
85039
86709
  const c = q2(options.column);
85040
86710
  if (options.type) {
85041
86711
  const nullable = options.dropNullable ? " NOT NULL" : "";
85042
86712
  const def = options.setDefault !== undefined ? ` DEFAULT ${options.setDefault}` : "";
85043
- statements.push(`ALTER TABLE ${t6} MODIFY COLUMN ${c} ${options.type.toUpperCase()}${nullable}${def};`);
86713
+ statements.push(`ALTER TABLE ${t2} MODIFY COLUMN ${c} ${options.type.toUpperCase()}${nullable}${def};`);
85044
86714
  warnings.push("MySQL MODIFY COLUMN resets column attributes not explicitly specified");
85045
86715
  } else {
85046
86716
  if (options.setDefault !== undefined) {
85047
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} SET DEFAULT ${options.setDefault};`);
86717
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} SET DEFAULT ${options.setDefault};`);
85048
86718
  }
85049
86719
  if (options.dropDefault) {
85050
- statements.push(`ALTER TABLE ${t6} ALTER COLUMN ${c} DROP DEFAULT;`);
86720
+ statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} DROP DEFAULT;`);
85051
86721
  }
85052
86722
  if (options.setNullable) {
85053
86723
  warnings.push("MySQL requires MODIFY COLUMN with full type to change nullability \u2014 use --type to specify");
@@ -85057,7 +86727,7 @@ class MySQLDDLGenerator {
85057
86727
  }
85058
86728
  }
85059
86729
  if (options.rename) {
85060
- statements.push(`ALTER TABLE ${t6} RENAME COLUMN ${c} TO ${q2(options.rename)};`);
86730
+ statements.push(`ALTER TABLE ${t2} RENAME COLUMN ${c} TO ${q2(options.rename)};`);
85061
86731
  }
85062
86732
  if (statements.length === 0 && warnings.length === 0) {
85063
86733
  warnings.push("No alter operations specified");
@@ -85066,12 +86736,12 @@ class MySQLDDLGenerator {
85066
86736
  `), warnings };
85067
86737
  }
85068
86738
  addIndex(index) {
85069
- const unique = index.unique ? "UNIQUE " : "";
86739
+ const unique2 = index.unique ? "UNIQUE " : "";
85070
86740
  const name = index.name || `idx_${index.table}_${index.columns.join("_")}`;
85071
86741
  const using = index.type ? ` USING ${index.type.toUpperCase()}` : "";
85072
86742
  const cols = index.columns.map(q2).join(", ");
85073
86743
  return {
85074
- sql: `CREATE ${unique}INDEX ${q2(name)} ON ${q2(index.table)} (${cols})${using};`,
86744
+ sql: `CREATE ${unique2}INDEX ${q2(name)} ON ${q2(index.table)} (${cols})${using};`,
85075
86745
  warnings: []
85076
86746
  };
85077
86747
  }
@@ -85083,14 +86753,14 @@ class MySQLDDLGenerator {
85083
86753
  };
85084
86754
  }
85085
86755
  addConstraint(constraint) {
85086
- const t6 = q2(constraint.table);
86756
+ const t2 = q2(constraint.table);
85087
86757
  const warnings = [];
85088
86758
  switch (constraint.type) {
85089
86759
  case "foreign_key": {
85090
86760
  const name = constraint.name || `fk_${constraint.table}_${constraint.column}`;
85091
86761
  const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
85092
86762
  return {
85093
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q2(name)} FOREIGN KEY (${q2(constraint.column)}) REFERENCES ${q2(constraint.references.table)}(${q2(constraint.references.column)})${onDelete};`,
86763
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name)} FOREIGN KEY (${q2(constraint.column)}) REFERENCES ${q2(constraint.references.table)}(${q2(constraint.references.column)})${onDelete};`,
85094
86764
  warnings
85095
86765
  };
85096
86766
  }
@@ -85098,14 +86768,14 @@ class MySQLDDLGenerator {
85098
86768
  const name = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
85099
86769
  const cols = constraint.columns.map(q2).join(", ");
85100
86770
  return {
85101
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q2(name)} UNIQUE (${cols});`,
86771
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name)} UNIQUE (${cols});`,
85102
86772
  warnings
85103
86773
  };
85104
86774
  }
85105
86775
  case "check": {
85106
86776
  const name = constraint.name || `ck_${constraint.table}`;
85107
86777
  return {
85108
- sql: `ALTER TABLE ${t6} ADD CONSTRAINT ${q2(name)} CHECK (${constraint.expression});`,
86778
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name)} CHECK (${constraint.expression});`,
85109
86779
  warnings: ["CHECK constraints enforced in MySQL 8.0.16+ and MariaDB 10.2.1+ only"]
85110
86780
  };
85111
86781
  }
@@ -85618,7 +87288,7 @@ addExecOpts(migrateCommand.command("drop-enum <name>").description(t("migrate.dr
85618
87288
  });
85619
87289
 
85620
87290
  // src/commands/use.ts
85621
- import { join as join14 } from "path";
87291
+ import { join as join17 } from "path";
85622
87292
  async function switchDefault(configPath, name, config) {
85623
87293
  if (!config.connections[name]) {
85624
87294
  const available = Object.keys(config.connections).join(", ");
@@ -85641,7 +87311,7 @@ function listConnectionsForDisplay(config) {
85641
87311
  }
85642
87312
  async function ensureV2Config(configPath) {
85643
87313
  const storagePath = await resolveConfigStoragePath(configPath);
85644
- const configFile = Bun.file(join14(storagePath, "config.json"));
87314
+ const configFile = Bun.file(join17(storagePath, "config.json"));
85645
87315
  const legacyFile = Bun.file(configPath);
85646
87316
  if (!await configFile.exists() && !await legacyFile.exists()) {
85647
87317
  throw new ConfigError(t("init.config_not_found"));
@@ -85703,8 +87373,11 @@ var useCommand = new Command("use").description("Switch or display the default d
85703
87373
  });
85704
87374
 
85705
87375
  // src/cli.ts
85706
- import { join as join15 } from "path";
87376
+ import { join as join18 } from "path";
85707
87377
  var _bgVersionCheckResult;
87378
+ function shouldSkipBackgroundChecks() {
87379
+ return process.env.DBCLI_NO_UPDATE_CHECK === "1" || process.env.DBCLI_NO_UPDATE_CHECK === "true" || false;
87380
+ }
85708
87381
  var program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--no-color", "Disable colored output").option("-v, --verbose", "Increase verbosity (-v verbose, -vv debug)", (_, prev) => prev + 1, 0).option("-q, --quiet", "Suppress non-essential output").option("--config <path>", "Path to .dbcli config file", ".dbcli").option("--use <connection>", "Use a specific named connection (v2 config)");
85709
87382
  program2.hook("preAction", (thisCommand, actionCommand) => {
85710
87383
  const opts = thisCommand.opts();
@@ -85723,13 +87396,13 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
85723
87396
  }
85724
87397
  setGlobalLogger(createLogger(level));
85725
87398
  const isUpgradeCommand = actionCommand.name() === "upgrade";
85726
- if (!opts.quiet && !isUpgradeCommand) {
87399
+ if (!opts.quiet && !isUpgradeCommand && !shouldSkipBackgroundChecks()) {
85727
87400
  const configPath = resolveConfigPath(actionCommand);
85728
87401
  (async () => {
85729
87402
  try {
85730
87403
  let cache = null;
85731
87404
  try {
85732
- const cacheFile = Bun.file(join15(configPath, "version-check.json"));
87405
+ const cacheFile = Bun.file(join18(configPath, "version-check.json"));
85733
87406
  if (await cacheFile.exists()) {
85734
87407
  cache = await cacheFile.json();
85735
87408
  }
@@ -85748,7 +87421,7 @@ program2.hook("postAction", async (thisCommand, actionCommand) => {
85748
87421
  `);
85749
87422
  }
85750
87423
  const isUpgradeOrSkill = ["upgrade", "skill"].includes(actionCommand.name());
85751
- if (!thisCommand.opts().quiet && !isUpgradeOrSkill) {
87424
+ if (!thisCommand.opts().quiet && !isUpgradeOrSkill && !shouldSkipBackgroundChecks()) {
85752
87425
  const outdatedSkills = await checkSkillUpdates();
85753
87426
  if (outdatedSkills.length > 0) {
85754
87427
  process.stderr.write(formatSkillUpdateReminder(outdatedSkills) + `
@@ -85767,6 +87440,12 @@ program2.command("query <sql>").description(t("query.description")).option("--fo
85767
87440
  process.exit(1);
85768
87441
  }
85769
87442
  });
87443
+ program2.command("plan <sql>").description("Analyze SQL risk without executing").option("--format <type>", "Output format: text, json", "text").action(async (sql, options, command) => {
87444
+ await planCommand(sql, options, command);
87445
+ });
87446
+ program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv", "table").option("--no-limit", "Disable size guard wrap (LIMIT 1000)").option("--dry-run", "Show final SQL + bind values; do not execute").option("--param <kv>", "Pass parameter as key=value (repeatable)", (val, prev = []) => prev.concat([val]), []).option("--param-file <path>", "JSON file containing param values").action(async (name, options, command) => {
87447
+ await qCommand(name, options, command);
87448
+ });
85770
87449
  program2.command("insert <table>").description(t("insert.description")).option("--data <json>", "JSON object to insert").option("--dry-run", "Show generated SQL without executing").option("--force", "Skip confirmation prompt").action(async (table, options, command) => {
85771
87450
  try {
85772
87451
  await insertCommand(table, options, command);
@@ -85819,6 +87498,7 @@ program2.addCommand(upgradeCommand);
85819
87498
  program2.addCommand(shellCommand);
85820
87499
  program2.addCommand(migrateCommand);
85821
87500
  program2.addCommand(useCommand);
87501
+ program2.addCommand(queriesCommand);
85822
87502
  if (!process.argv.slice(2).length) {
85823
87503
  program2.outputHelp();
85824
87504
  }