@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/CHANGELOG.md +12 -0
- package/README.md +58 -0
- package/README.zh-TW.md +54 -0
- package/assets/SKILL.md +42 -0
- package/assets/reference.md +142 -3
- package/assets/snippets/.keep +0 -0
- package/assets/snippets/README.md +18 -0
- package/assets/snippets/diag/cache-hit.mysql.sql +19 -0
- package/assets/snippets/diag/cache-hit.postgres.sql +18 -0
- package/assets/snippets/diag/connections.mysql.sql +16 -0
- package/assets/snippets/diag/connections.postgres.sql +16 -0
- package/assets/snippets/diag/db-size.mysql.sql +10 -0
- package/assets/snippets/diag/db-size.postgres.sql +9 -0
- package/assets/snippets/diag/index-usage.mysql.sql +15 -0
- package/assets/snippets/diag/index-usage.postgres.sql +14 -0
- package/assets/snippets/diag/locks.mysql.sql +14 -0
- package/assets/snippets/diag/locks.postgres.sql +15 -0
- package/assets/snippets/diag/long-running.mysql.sql +14 -0
- package/assets/snippets/diag/long-running.postgres.sql +19 -0
- package/assets/snippets/diag/missing-indexes.mysql.sql +13 -0
- package/assets/snippets/diag/missing-indexes.postgres.sql +15 -0
- package/assets/snippets/diag/table-sizes.mysql.sql +14 -0
- package/assets/snippets/diag/table-sizes.postgres.sql +13 -0
- package/dist/cli.mjs +1859 -179
- package/package.json +2 -2
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: () =>
|
|
7531
|
+
SchemaLayeredLoader: () => SchemaLayeredLoader
|
|
7532
7532
|
});
|
|
7533
7533
|
import { join as join6 } from "path";
|
|
7534
7534
|
|
|
7535
|
-
class
|
|
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.
|
|
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:
|
|
78182
|
-
const loader = new
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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:
|
|
80923
|
-
const loader = new
|
|
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:
|
|
81014
|
-
const loader = new
|
|
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,
|
|
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
|
|
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, {
|
|
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
|
|
84237
|
+
} catch {
|
|
82572
84238
|
try {
|
|
82573
|
-
const { mkdir } = await import("fs/promises");
|
|
82574
|
-
await
|
|
82575
|
-
} catch
|
|
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((
|
|
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
|
|
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,
|
|
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
|
|
82796
|
-
if (blacklistedTables.has(
|
|
82797
|
-
skipped.push(`${
|
|
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 (
|
|
82801
|
-
skipped.push(`${
|
|
84466
|
+
if (t2.tableType === "view") {
|
|
84467
|
+
skipped.push(`${t2.name} (view)`);
|
|
82802
84468
|
continue;
|
|
82803
84469
|
}
|
|
82804
|
-
const category = getSizeCategory(
|
|
84470
|
+
const category = getSizeCategory(t2.estimatedRowCount);
|
|
82805
84471
|
if (category === "huge" && !options.includeLarge) {
|
|
82806
|
-
skipped.push(`${
|
|
84472
|
+
skipped.push(`${t2.name} (~${(t2.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
|
|
82807
84473
|
continue;
|
|
82808
84474
|
}
|
|
82809
|
-
const schema = await adapter.getTableSchema(
|
|
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
|
|
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((
|
|
82894
|
-
const removedTables = Array.from(beforeTables).filter((
|
|
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((
|
|
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,
|
|
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
|
|
83006
|
-
if (
|
|
84671
|
+
for (const t2 of tables) {
|
|
84672
|
+
if (t2.tableType === "view")
|
|
83007
84673
|
continue;
|
|
83008
|
-
const schema = await adapter.getTableSchema(
|
|
83009
|
-
currentSnapshot.tables[
|
|
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
|
|
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,
|
|
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
|
|
84788
|
+
import { join as join14 } from "path";
|
|
83123
84789
|
import { resolveSrv as resolveSrv2 } from "dns/promises";
|
|
83124
|
-
var
|
|
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(
|
|
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((
|
|
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((
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
83556
|
-
tableColumns.set(
|
|
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 =
|
|
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
|
|
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
|
|
85377
|
+
return join15(home, ".bashrc");
|
|
83710
85378
|
case "zsh":
|
|
83711
|
-
return
|
|
85379
|
+
return join15(home, ".zshrc");
|
|
83712
85380
|
case "fish":
|
|
83713
|
-
return
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
|
86159
|
+
const result = await this.adapter.execute(sql);
|
|
84493
86160
|
const elapsed = Date.now() - startTime;
|
|
84494
|
-
const
|
|
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
|
|
84539
|
-
|
|
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
|
|
84603
|
-
|
|
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((
|
|
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 =
|
|
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
|
|
86543
|
+
const t2 = q(options.table);
|
|
84874
86544
|
const c = q(options.column);
|
|
84875
86545
|
if (options.type) {
|
|
84876
|
-
statements.push(`ALTER TABLE ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
86555
|
+
statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} DROP DEFAULT;`);
|
|
84886
86556
|
}
|
|
84887
86557
|
if (options.setNullable) {
|
|
84888
|
-
statements.push(`ALTER TABLE ${
|
|
86558
|
+
statements.push(`ALTER TABLE ${t2} ALTER COLUMN ${c} DROP NOT NULL;`);
|
|
84889
86559
|
}
|
|
84890
86560
|
if (options.dropNullable) {
|
|
84891
|
-
statements.push(`ALTER TABLE ${
|
|
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
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
}
|