@carllee1983/dbcli 0.2.0-beta → 0.3.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +653 -167
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -42686,6 +42686,60 @@ var require_table = __commonJS((exports, module) => {
|
|
|
42686
42686
|
module.exports = Table;
|
|
42687
42687
|
});
|
|
42688
42688
|
|
|
42689
|
+
// src/core/size-category.ts
|
|
42690
|
+
var exports_size_category = {};
|
|
42691
|
+
__export(exports_size_category, {
|
|
42692
|
+
getSizeCategory: () => getSizeCategory
|
|
42693
|
+
});
|
|
42694
|
+
function getSizeCategory(estimatedRowCount) {
|
|
42695
|
+
const count = estimatedRowCount ?? 0;
|
|
42696
|
+
for (const [threshold, category] of THRESHOLDS) {
|
|
42697
|
+
if (count >= threshold)
|
|
42698
|
+
return category;
|
|
42699
|
+
}
|
|
42700
|
+
return "small";
|
|
42701
|
+
}
|
|
42702
|
+
var THRESHOLDS;
|
|
42703
|
+
var init_size_category = __esm(() => {
|
|
42704
|
+
THRESHOLDS = [
|
|
42705
|
+
[1e6, "huge"],
|
|
42706
|
+
[1e5, "large"],
|
|
42707
|
+
[1e4, "medium"]
|
|
42708
|
+
];
|
|
42709
|
+
});
|
|
42710
|
+
|
|
42711
|
+
// src/commands/query-size-guard.ts
|
|
42712
|
+
var exports_query_size_guard = {};
|
|
42713
|
+
__export(exports_query_size_guard, {
|
|
42714
|
+
shouldBlockQuery: () => shouldBlockQuery
|
|
42715
|
+
});
|
|
42716
|
+
function shouldBlockQuery(sql, tableSizeInfo) {
|
|
42717
|
+
if (!tableSizeInfo) {
|
|
42718
|
+
return { blocked: false };
|
|
42719
|
+
}
|
|
42720
|
+
const category = getSizeCategory(tableSizeInfo.estimatedRowCount);
|
|
42721
|
+
if (category !== "huge") {
|
|
42722
|
+
return { blocked: false, sizeCategory: category };
|
|
42723
|
+
}
|
|
42724
|
+
const normalizedSql = sql.trim().toUpperCase();
|
|
42725
|
+
if (!normalizedSql.startsWith("SELECT")) {
|
|
42726
|
+
return { blocked: false, sizeCategory: category };
|
|
42727
|
+
}
|
|
42728
|
+
const hasWhere = /\bWHERE\b/i.test(sql);
|
|
42729
|
+
const hasLimit = /\bLIMIT\b/i.test(sql);
|
|
42730
|
+
if (hasWhere || hasLimit) {
|
|
42731
|
+
return { blocked: false, sizeCategory: category };
|
|
42732
|
+
}
|
|
42733
|
+
return {
|
|
42734
|
+
blocked: true,
|
|
42735
|
+
sizeCategory: category,
|
|
42736
|
+
reason: `Table has ~${tableSizeInfo.estimatedRowCount.toLocaleString()} rows (huge). Add WHERE or LIMIT clause, or use --no-limit to override.`
|
|
42737
|
+
};
|
|
42738
|
+
}
|
|
42739
|
+
var init_query_size_guard = __esm(() => {
|
|
42740
|
+
init_size_category();
|
|
42741
|
+
});
|
|
42742
|
+
|
|
42689
42743
|
// node_modules/commander/esm.mjs
|
|
42690
42744
|
var import__ = __toESM(require_commander(), 1);
|
|
42691
42745
|
var {
|
|
@@ -42704,7 +42758,7 @@ var {
|
|
|
42704
42758
|
// package.json
|
|
42705
42759
|
var package_default = {
|
|
42706
42760
|
name: "@carllee1983/dbcli",
|
|
42707
|
-
version: "0.
|
|
42761
|
+
version: "0.3.0-beta",
|
|
42708
42762
|
description: "Database CLI for AI agents",
|
|
42709
42763
|
type: "module",
|
|
42710
42764
|
publishConfig: {
|
|
@@ -47358,18 +47412,28 @@ class PostgreSQLAdapter {
|
|
|
47358
47412
|
try {
|
|
47359
47413
|
const query = `
|
|
47360
47414
|
SELECT
|
|
47361
|
-
|
|
47362
|
-
|
|
47363
|
-
|
|
47364
|
-
|
|
47365
|
-
|
|
47415
|
+
c.relname as table_name,
|
|
47416
|
+
c.reltuples::bigint as estimated_rows,
|
|
47417
|
+
CASE c.relkind
|
|
47418
|
+
WHEN 'r' THEN 'table'
|
|
47419
|
+
WHEN 'v' THEN 'view'
|
|
47420
|
+
WHEN 'm' THEN 'view'
|
|
47421
|
+
ELSE 'table'
|
|
47422
|
+
END as table_type
|
|
47423
|
+
FROM pg_class c
|
|
47424
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
47425
|
+
WHERE n.nspname = 'public'
|
|
47426
|
+
AND c.relkind IN ('r', 'v', 'm')
|
|
47427
|
+
ORDER BY c.relname
|
|
47366
47428
|
`;
|
|
47367
47429
|
const results = await this.execute(query);
|
|
47368
47430
|
return results.map((row) => ({
|
|
47369
47431
|
name: row.table_name,
|
|
47370
47432
|
columns: [],
|
|
47371
|
-
rowCount: row.
|
|
47372
|
-
engine: "PostgreSQL"
|
|
47433
|
+
rowCount: Math.max(0, row.estimated_rows || 0),
|
|
47434
|
+
engine: "PostgreSQL",
|
|
47435
|
+
estimatedRowCount: Math.max(0, row.estimated_rows || 0),
|
|
47436
|
+
tableType: row.table_type
|
|
47373
47437
|
}));
|
|
47374
47438
|
} catch (error) {
|
|
47375
47439
|
throw mapError(error, "postgresql", this.options);
|
|
@@ -47391,19 +47455,40 @@ class PostgreSQLAdapter {
|
|
|
47391
47455
|
SELECT 1 FROM information_schema.table_constraints tc
|
|
47392
47456
|
JOIN information_schema.key_column_usage kcu
|
|
47393
47457
|
ON tc.constraint_name = kcu.constraint_name
|
|
47394
|
-
WHERE tc.table_name =
|
|
47458
|
+
WHERE tc.table_name = $1
|
|
47459
|
+
AND tc.table_schema = 'public'
|
|
47395
47460
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
47396
47461
|
AND kcu.column_name = c.column_name
|
|
47397
47462
|
)
|
|
47398
|
-
) as is_primary_key
|
|
47463
|
+
) as is_primary_key,
|
|
47464
|
+
(c.column_default LIKE 'nextval%') as auto_increment,
|
|
47465
|
+
pgd.description as comment
|
|
47399
47466
|
FROM information_schema.columns c
|
|
47400
|
-
JOIN
|
|
47401
|
-
ON c.
|
|
47467
|
+
LEFT JOIN pg_catalog.pg_statio_all_tables st
|
|
47468
|
+
ON st.schemaname = c.table_schema AND st.relname = c.table_name
|
|
47469
|
+
LEFT JOIN pg_catalog.pg_description pgd
|
|
47470
|
+
ON pgd.objoid = st.relid AND pgd.objsubid = c.ordinal_position
|
|
47402
47471
|
WHERE c.table_name = $1
|
|
47403
|
-
AND
|
|
47472
|
+
AND c.table_schema = 'public'
|
|
47404
47473
|
ORDER BY c.ordinal_position
|
|
47405
47474
|
`;
|
|
47406
47475
|
const columns = await this.execute(columnQuery, [tableName]);
|
|
47476
|
+
const enumQuery = `
|
|
47477
|
+
SELECT
|
|
47478
|
+
c.column_name as name,
|
|
47479
|
+
array_agg(e.enumlabel ORDER BY e.enumsortorder) as enum_values
|
|
47480
|
+
FROM information_schema.columns c
|
|
47481
|
+
JOIN pg_type t ON t.typname = c.udt_name
|
|
47482
|
+
JOIN pg_enum e ON e.enumtypid = t.oid
|
|
47483
|
+
WHERE c.table_name = $1
|
|
47484
|
+
AND c.table_schema = 'public'
|
|
47485
|
+
GROUP BY c.column_name
|
|
47486
|
+
`;
|
|
47487
|
+
const enumResults = await this.execute(enumQuery, [tableName]);
|
|
47488
|
+
const enumMap = new Map;
|
|
47489
|
+
for (const row of enumResults) {
|
|
47490
|
+
enumMap.set(row.name, Array.isArray(row.enum_values) ? row.enum_values : []);
|
|
47491
|
+
}
|
|
47407
47492
|
const fkQuery = `
|
|
47408
47493
|
SELECT
|
|
47409
47494
|
tc.constraint_name as name,
|
|
@@ -47425,6 +47510,28 @@ class PostgreSQLAdapter {
|
|
|
47425
47510
|
fkMap.set(fk.columns[0], { table: fk.ref_table, column: fk.ref_columns[0] });
|
|
47426
47511
|
}
|
|
47427
47512
|
}
|
|
47513
|
+
const indexQuery = `
|
|
47514
|
+
SELECT
|
|
47515
|
+
i.relname as name,
|
|
47516
|
+
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
|
|
47517
|
+
ix.indisunique as is_unique
|
|
47518
|
+
FROM pg_index ix
|
|
47519
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
47520
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
47521
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
47522
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
47523
|
+
WHERE t.relname = $1
|
|
47524
|
+
AND n.nspname = 'public'
|
|
47525
|
+
AND NOT ix.indisprimary
|
|
47526
|
+
GROUP BY i.relname, ix.indisunique
|
|
47527
|
+
`;
|
|
47528
|
+
const indexResults = await this.execute(indexQuery, [tableName]);
|
|
47529
|
+
const estimateQuery = `
|
|
47530
|
+
SELECT reltuples::bigint as estimated_rows
|
|
47531
|
+
FROM pg_class
|
|
47532
|
+
WHERE relname = $1
|
|
47533
|
+
`;
|
|
47534
|
+
const estimateResults = await this.execute(estimateQuery, [tableName]);
|
|
47428
47535
|
const pkQuery = `
|
|
47429
47536
|
SELECT array_agg(a.attname) as columns
|
|
47430
47537
|
FROM (
|
|
@@ -47452,12 +47559,21 @@ class PostgreSQLAdapter {
|
|
|
47452
47559
|
nullable: col.nullable,
|
|
47453
47560
|
default: col.default_value || undefined,
|
|
47454
47561
|
primaryKey: col.is_primary_key,
|
|
47455
|
-
foreignKey: fkMap.get(col.name)
|
|
47562
|
+
foreignKey: fkMap.get(col.name),
|
|
47563
|
+
autoIncrement: col.auto_increment,
|
|
47564
|
+
comment: col.comment || null,
|
|
47565
|
+
enumValues: enumMap.get(col.name)
|
|
47456
47566
|
})),
|
|
47457
47567
|
rowCount: countResult[0]?.count || 0,
|
|
47458
47568
|
engine: "PostgreSQL",
|
|
47459
47569
|
primaryKey: primaryKeyArray,
|
|
47460
|
-
foreignKeys: safeForeignKeys
|
|
47570
|
+
foreignKeys: safeForeignKeys,
|
|
47571
|
+
indexes: indexResults.map((idx) => ({
|
|
47572
|
+
name: idx.name,
|
|
47573
|
+
columns: Array.isArray(idx.columns) ? idx.columns : [],
|
|
47574
|
+
unique: idx.is_unique
|
|
47575
|
+
})),
|
|
47576
|
+
estimatedRowCount: Math.max(0, estimateResults[0]?.estimated_rows || 0)
|
|
47461
47577
|
};
|
|
47462
47578
|
return schema;
|
|
47463
47579
|
} catch (error) {
|
|
@@ -47468,6 +47584,13 @@ class PostgreSQLAdapter {
|
|
|
47468
47584
|
|
|
47469
47585
|
// src/adapters/mysql-adapter.ts
|
|
47470
47586
|
var import_promise = __toESM(require_promise(), 1);
|
|
47587
|
+
function parseEnumValues(columnType) {
|
|
47588
|
+
const match = columnType.match(/^enum\((.+)\)$/i);
|
|
47589
|
+
if (!match)
|
|
47590
|
+
return;
|
|
47591
|
+
return match[1].split(",").map((v) => v.trim().replace(/^'|'$/g, ""));
|
|
47592
|
+
}
|
|
47593
|
+
|
|
47471
47594
|
class MySQLAdapter {
|
|
47472
47595
|
db = null;
|
|
47473
47596
|
options;
|
|
@@ -47533,12 +47656,13 @@ class MySQLAdapter {
|
|
|
47533
47656
|
t.TABLE_NAME as table_name,
|
|
47534
47657
|
t.TABLE_ROWS as row_count,
|
|
47535
47658
|
t.ENGINE as engine,
|
|
47659
|
+
t.TABLE_TYPE as table_type,
|
|
47536
47660
|
COUNT(c.COLUMN_NAME) as column_count
|
|
47537
47661
|
FROM information_schema.TABLES t
|
|
47538
47662
|
LEFT JOIN information_schema.COLUMNS c
|
|
47539
47663
|
ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
|
|
47540
47664
|
WHERE t.TABLE_SCHEMA = DATABASE()
|
|
47541
|
-
GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE
|
|
47665
|
+
GROUP BY t.TABLE_NAME, t.TABLE_ROWS, t.ENGINE, t.TABLE_TYPE
|
|
47542
47666
|
ORDER BY t.TABLE_NAME
|
|
47543
47667
|
`;
|
|
47544
47668
|
const results = await this.execute(query);
|
|
@@ -47546,7 +47670,9 @@ class MySQLAdapter {
|
|
|
47546
47670
|
name: row.table_name,
|
|
47547
47671
|
columns: Array(row.column_count).fill(null),
|
|
47548
47672
|
rowCount: row.row_count || 0,
|
|
47549
|
-
engine: row.engine
|
|
47673
|
+
engine: row.engine,
|
|
47674
|
+
estimatedRowCount: row.row_count || 0,
|
|
47675
|
+
tableType: row.table_type === "VIEW" ? "view" : "table"
|
|
47550
47676
|
}));
|
|
47551
47677
|
} catch (error) {
|
|
47552
47678
|
throw mapError(error, this.system, this.options);
|
|
@@ -47563,7 +47689,9 @@ class MySQLAdapter {
|
|
|
47563
47689
|
COLUMN_TYPE as type,
|
|
47564
47690
|
IS_NULLABLE = 'YES' as nullable,
|
|
47565
47691
|
COLUMN_DEFAULT as default_value,
|
|
47566
|
-
COLUMN_KEY = 'PRI' as is_primary_key
|
|
47692
|
+
COLUMN_KEY = 'PRI' as is_primary_key,
|
|
47693
|
+
EXTRA LIKE '%auto_increment%' as auto_increment,
|
|
47694
|
+
COLUMN_COMMENT as comment
|
|
47567
47695
|
FROM information_schema.COLUMNS
|
|
47568
47696
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
47569
47697
|
AND TABLE_NAME = ?
|
|
@@ -47592,6 +47720,18 @@ class MySQLAdapter {
|
|
|
47592
47720
|
}
|
|
47593
47721
|
}
|
|
47594
47722
|
const primaryKeyColumns = columns.filter((col) => col.is_primary_key).map((col) => col.name);
|
|
47723
|
+
const indexQuery = `
|
|
47724
|
+
SELECT
|
|
47725
|
+
INDEX_NAME as name,
|
|
47726
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
47727
|
+
NOT NON_UNIQUE as is_unique
|
|
47728
|
+
FROM information_schema.STATISTICS
|
|
47729
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
47730
|
+
AND TABLE_NAME = ?
|
|
47731
|
+
AND INDEX_NAME != 'PRIMARY'
|
|
47732
|
+
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
47733
|
+
`;
|
|
47734
|
+
const indexResults = await this.execute(indexQuery, [tableName]);
|
|
47595
47735
|
const countResult = await this.execute(`SELECT COUNT(*) as count FROM \`${tableName}\``);
|
|
47596
47736
|
const tableQuery = `
|
|
47597
47737
|
SELECT ENGINE as engine
|
|
@@ -47599,6 +47739,12 @@ class MySQLAdapter {
|
|
|
47599
47739
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
47600
47740
|
`;
|
|
47601
47741
|
const tableResults = await this.execute(tableQuery, [tableName]);
|
|
47742
|
+
const estimateQuery = `
|
|
47743
|
+
SELECT TABLE_ROWS as estimated_rows
|
|
47744
|
+
FROM information_schema.TABLES
|
|
47745
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?
|
|
47746
|
+
`;
|
|
47747
|
+
const estimateResults = await this.execute(estimateQuery, [tableName]);
|
|
47602
47748
|
const schema = {
|
|
47603
47749
|
name: tableName,
|
|
47604
47750
|
columns: columns.map((col) => ({
|
|
@@ -47607,7 +47753,10 @@ class MySQLAdapter {
|
|
|
47607
47753
|
nullable: col.nullable,
|
|
47608
47754
|
default: col.default_value || undefined,
|
|
47609
47755
|
primaryKey: col.is_primary_key,
|
|
47610
|
-
foreignKey: fkMap.get(col.name)
|
|
47756
|
+
foreignKey: fkMap.get(col.name),
|
|
47757
|
+
autoIncrement: col.auto_increment,
|
|
47758
|
+
comment: col.comment || null,
|
|
47759
|
+
enumValues: parseEnumValues(col.type)
|
|
47611
47760
|
})),
|
|
47612
47761
|
rowCount: countResult[0]?.count || 0,
|
|
47613
47762
|
engine: tableResults[0]?.engine || "MySQL",
|
|
@@ -47617,7 +47766,13 @@ class MySQLAdapter {
|
|
|
47617
47766
|
columns: fk.columns.split(",").map((c) => c.trim()),
|
|
47618
47767
|
refTable: fk.ref_table,
|
|
47619
47768
|
refColumns: fk.ref_columns.split(",").map((c) => c.trim())
|
|
47620
|
-
}))
|
|
47769
|
+
})),
|
|
47770
|
+
indexes: indexResults.map((idx) => ({
|
|
47771
|
+
name: idx.name,
|
|
47772
|
+
columns: idx.columns.split(",").map((c) => c.trim()),
|
|
47773
|
+
unique: idx.is_unique
|
|
47774
|
+
})),
|
|
47775
|
+
estimatedRowCount: estimateResults[0]?.estimated_rows || 0
|
|
47621
47776
|
};
|
|
47622
47777
|
return schema;
|
|
47623
47778
|
} catch (error) {
|
|
@@ -47890,6 +48045,8 @@ class TableListFormatter {
|
|
|
47890
48045
|
}
|
|
47891
48046
|
}
|
|
47892
48047
|
// src/formatters/json-formatter.ts
|
|
48048
|
+
init_size_category();
|
|
48049
|
+
|
|
47893
48050
|
class JSONFormatter {
|
|
47894
48051
|
format(data, options) {
|
|
47895
48052
|
const spacing = options?.compact ? undefined : 2;
|
|
@@ -47900,11 +48057,16 @@ class JSONFormatter {
|
|
|
47900
48057
|
class TableSchemaJSONFormatter {
|
|
47901
48058
|
format(table, options) {
|
|
47902
48059
|
const spacing = options?.compact ? undefined : 2;
|
|
48060
|
+
const rowCount = table.estimatedRowCount ?? table.rowCount;
|
|
47903
48061
|
const output = {
|
|
47904
48062
|
name: table.name,
|
|
48063
|
+
estimatedRowCount: rowCount ?? null,
|
|
48064
|
+
sizeCategory: rowCount !== undefined && rowCount !== null ? getSizeCategory(rowCount) : null,
|
|
48065
|
+
tableType: table.tableType ?? "table",
|
|
47905
48066
|
columns: table.columns,
|
|
47906
48067
|
primaryKey: table.primaryKey || [],
|
|
47907
48068
|
foreignKeys: table.foreignKeys || [],
|
|
48069
|
+
indexes: table.indexes || [],
|
|
47908
48070
|
metadata: {
|
|
47909
48071
|
rowCount: table.rowCount,
|
|
47910
48072
|
engine: table.engine
|
|
@@ -48055,8 +48217,11 @@ async function listAction(options) {
|
|
|
48055
48217
|
const formatter = new TableListFormatter;
|
|
48056
48218
|
console.log(formatter.format(tables));
|
|
48057
48219
|
}
|
|
48220
|
+
const tableCount = tables.filter((t2) => t2.tableType !== "view").length;
|
|
48221
|
+
const viewCount = tables.filter((t2) => t2.tableType === "view").length;
|
|
48222
|
+
const viewSuffix = viewCount > 0 ? ` (${viewCount} views)` : "";
|
|
48058
48223
|
console.log(`
|
|
48059
|
-
\u2713 Found ${
|
|
48224
|
+
\u2713 Found ${tableCount} tables${viewSuffix}`);
|
|
48060
48225
|
} finally {
|
|
48061
48226
|
await adapter.disconnect();
|
|
48062
48227
|
}
|
|
@@ -48197,6 +48362,19 @@ Row count: ~${schema.rowCount.toLocaleString()}`);
|
|
|
48197
48362
|
if (schema.engine) {
|
|
48198
48363
|
console.log(`Engine: ${schema.engine}`);
|
|
48199
48364
|
}
|
|
48365
|
+
if (schema.estimatedRowCount !== undefined) {
|
|
48366
|
+
const { getSizeCategory: getSizeCategory2 } = await Promise.resolve().then(() => (init_size_category(), exports_size_category));
|
|
48367
|
+
const category = getSizeCategory2(schema.estimatedRowCount);
|
|
48368
|
+
console.log(`Estimated rows: ~${schema.estimatedRowCount.toLocaleString()} (${category})`);
|
|
48369
|
+
}
|
|
48370
|
+
if (schema.indexes && schema.indexes.length > 0) {
|
|
48371
|
+
console.log(`
|
|
48372
|
+
Indexes:`);
|
|
48373
|
+
schema.indexes.forEach((idx) => {
|
|
48374
|
+
const uniqueTag = idx.unique ? " [UNIQUE]" : "";
|
|
48375
|
+
console.log(` ${idx.name}: (${idx.columns.join(", ")})${uniqueTag}`);
|
|
48376
|
+
});
|
|
48377
|
+
}
|
|
48200
48378
|
}
|
|
48201
48379
|
}
|
|
48202
48380
|
async function handleSchemaRefresh(adapter, config, options) {
|
|
@@ -48260,7 +48438,10 @@ async function handleSchemaReset(adapter, config, options) {
|
|
|
48260
48438
|
rowCount: fullSchema.rowCount,
|
|
48261
48439
|
engine: fullSchema.engine,
|
|
48262
48440
|
primaryKey: fullSchema.primaryKey || [],
|
|
48263
|
-
foreignKeys: fullSchema.foreignKeys || []
|
|
48441
|
+
foreignKeys: fullSchema.foreignKeys || [],
|
|
48442
|
+
indexes: fullSchema.indexes || [],
|
|
48443
|
+
estimatedRowCount: fullSchema.estimatedRowCount || 0,
|
|
48444
|
+
tableType: fullSchema.tableType || "table"
|
|
48264
48445
|
};
|
|
48265
48446
|
processed++;
|
|
48266
48447
|
if (processed % 10 === 0 || processed === tables.length) {
|
|
@@ -48299,7 +48480,10 @@ async function handleFullDatabaseScan(adapter, config, options) {
|
|
|
48299
48480
|
rowCount: fullSchema.rowCount,
|
|
48300
48481
|
engine: fullSchema.engine,
|
|
48301
48482
|
primaryKey: fullSchema.primaryKey || [],
|
|
48302
|
-
foreignKeys: fullSchema.foreignKeys || []
|
|
48483
|
+
foreignKeys: fullSchema.foreignKeys || [],
|
|
48484
|
+
indexes: fullSchema.indexes || [],
|
|
48485
|
+
estimatedRowCount: fullSchema.estimatedRowCount || 0,
|
|
48486
|
+
tableType: fullSchema.tableType || "table"
|
|
48303
48487
|
};
|
|
48304
48488
|
processed++;
|
|
48305
48489
|
if (processed % 10 === 0 || processed === tables.length) {
|
|
@@ -48980,6 +49164,18 @@ async function queryCommand(sql, options) {
|
|
|
48980
49164
|
if (!config.connection) {
|
|
48981
49165
|
throw new Error('Run "dbcli init" first');
|
|
48982
49166
|
}
|
|
49167
|
+
const mainTable = extractMainTable(sql);
|
|
49168
|
+
if (mainTable && config.schema && !options.noLimit) {
|
|
49169
|
+
const tableSchema = config.schema[mainTable];
|
|
49170
|
+
if (tableSchema) {
|
|
49171
|
+
const { shouldBlockQuery: shouldBlockQuery2 } = await Promise.resolve().then(() => (init_query_size_guard(), exports_query_size_guard));
|
|
49172
|
+
const guard = shouldBlockQuery2(sql, tableSchema);
|
|
49173
|
+
if (guard.blocked) {
|
|
49174
|
+
console.error(`\u26A0 ${guard.reason}`);
|
|
49175
|
+
process.exit(1);
|
|
49176
|
+
}
|
|
49177
|
+
}
|
|
49178
|
+
}
|
|
48983
49179
|
const adapter = AdapterFactory.createAdapter(config.connection);
|
|
48984
49180
|
await adapter.connect();
|
|
48985
49181
|
try {
|
|
@@ -49018,6 +49214,10 @@ async function queryCommand(sql, options) {
|
|
|
49018
49214
|
process.exit(1);
|
|
49019
49215
|
}
|
|
49020
49216
|
}
|
|
49217
|
+
function extractMainTable(sql) {
|
|
49218
|
+
const match = sql.match(/\bFROM\s+[`"']?(\w+)[`"']?/i);
|
|
49219
|
+
return match ? match[1] : null;
|
|
49220
|
+
}
|
|
49021
49221
|
|
|
49022
49222
|
// src/core/data-executor.ts
|
|
49023
49223
|
class DataExecutor {
|
|
@@ -49697,152 +49897,14 @@ async function exportCommand(sql, options) {
|
|
|
49697
49897
|
// src/commands/skill.ts
|
|
49698
49898
|
import * as path from "path";
|
|
49699
49899
|
import { homedir } from "os";
|
|
49700
|
-
|
|
49701
|
-
|
|
49702
|
-
class SkillGenerator {
|
|
49703
|
-
options;
|
|
49704
|
-
constructor(options) {
|
|
49705
|
-
this.options = options;
|
|
49706
|
-
}
|
|
49707
|
-
generateSkillMarkdown() {
|
|
49708
|
-
const commands = this.collectCommands();
|
|
49709
|
-
const filtered = this.filterByPermission(commands);
|
|
49710
|
-
return this.renderSkillMarkdown(filtered);
|
|
49711
|
-
}
|
|
49712
|
-
collectCommands() {
|
|
49713
|
-
const commands = [];
|
|
49714
|
-
this.options.program.commands.forEach((cmd) => {
|
|
49715
|
-
const skillCmd = {
|
|
49716
|
-
name: cmd.name(),
|
|
49717
|
-
description: cmd.description() || "No description",
|
|
49718
|
-
args: cmd.args || [],
|
|
49719
|
-
options: cmd.options.map((opt) => ({
|
|
49720
|
-
flag: opt.flags,
|
|
49721
|
-
description: opt.description || "No description",
|
|
49722
|
-
required: opt.required ?? false
|
|
49723
|
-
})),
|
|
49724
|
-
permissionLevel: this.detectPermissionLevel(cmd.name()),
|
|
49725
|
-
examples: this.generateExamples(cmd.name())
|
|
49726
|
-
};
|
|
49727
|
-
commands.push(skillCmd);
|
|
49728
|
-
});
|
|
49729
|
-
return commands;
|
|
49730
|
-
}
|
|
49731
|
-
detectPermissionLevel(commandName) {
|
|
49732
|
-
if (commandName === "delete")
|
|
49733
|
-
return "admin";
|
|
49734
|
-
if (["insert", "update"].includes(commandName))
|
|
49735
|
-
return "read-write";
|
|
49736
|
-
return "query-only";
|
|
49737
|
-
}
|
|
49738
|
-
filterByPermission(commands) {
|
|
49739
|
-
const permLevel = this.options.permissionLevel;
|
|
49740
|
-
return commands.filter((cmd) => {
|
|
49741
|
-
if (permLevel === "query-only") {
|
|
49742
|
-
return !["insert", "update", "delete"].includes(cmd.name);
|
|
49743
|
-
}
|
|
49744
|
-
if (permLevel === "read-write") {
|
|
49745
|
-
return cmd.name !== "delete";
|
|
49746
|
-
}
|
|
49747
|
-
return true;
|
|
49748
|
-
});
|
|
49749
|
-
}
|
|
49750
|
-
generateExamples(commandName) {
|
|
49751
|
-
const examples = {
|
|
49752
|
-
init: ["dbcli init"],
|
|
49753
|
-
list: ["dbcli list", "dbcli list --format json"],
|
|
49754
|
-
schema: ["dbcli schema users", "dbcli schema users --format json"],
|
|
49755
|
-
query: [
|
|
49756
|
-
'dbcli query "SELECT * FROM users LIMIT 10"',
|
|
49757
|
-
'dbcli query "SELECT id, email FROM users" --format json'
|
|
49758
|
-
],
|
|
49759
|
-
insert: [`dbcli insert users --data '{"name":"Alice","email":"alice@example.com"}'`],
|
|
49760
|
-
update: [`dbcli update users --where "id=1" --set '{"name":"Bob"}'`],
|
|
49761
|
-
delete: ['dbcli delete users --where "id=1" --force'],
|
|
49762
|
-
export: [
|
|
49763
|
-
'dbcli export "SELECT * FROM users" --format csv --output users.csv',
|
|
49764
|
-
`dbcli export "SELECT * FROM users" --format json | jq '.[]'`
|
|
49765
|
-
]
|
|
49766
|
-
};
|
|
49767
|
-
return examples[commandName] || [`dbcli ${commandName}`];
|
|
49768
|
-
}
|
|
49769
|
-
renderSkillMarkdown(commands) {
|
|
49770
|
-
const frontmatter = `---
|
|
49771
|
-
name: dbcli
|
|
49772
|
-
description: Database CLI for AI agents. Use to query, modify, and manage database schemas with permission-based access control.
|
|
49773
|
-
user-invocable: true
|
|
49774
|
-
allowed-tools: Bash(dbcli *)
|
|
49775
|
-
---`;
|
|
49776
|
-
const header = `# dbcli Skill Documentation
|
|
49777
|
-
|
|
49778
|
-
Database CLI for AI agents with permission-based access control.
|
|
49779
|
-
|
|
49780
|
-
## Commands`;
|
|
49781
|
-
const commandDocs = commands.map((cmd) => this.renderCommandSection(cmd)).join(`
|
|
49782
|
-
|
|
49783
|
-
`);
|
|
49784
|
-
const footer = `## Permission Levels
|
|
49785
|
-
|
|
49786
|
-
dbcli enforces permission-based access control:
|
|
49787
|
-
|
|
49788
|
-
- **Query-only**: Execute SELECT queries, list tables, view schemas, export data
|
|
49789
|
-
- **Read-Write**: Query-only + INSERT and UPDATE operations
|
|
49790
|
-
- **Admin**: Read-Write + DELETE operations
|
|
49791
|
-
|
|
49792
|
-
Your current permission level is set in \`.dbcli\` config.
|
|
49793
|
-
|
|
49794
|
-
## Tips for AI Agents
|
|
49795
|
-
|
|
49796
|
-
1. **Schema introspection first**: Start with \`dbcli schema <table>\` to understand structure
|
|
49797
|
-
2. **Test with --dry-run**: Use \`--dry-run\` to preview SQL before executing
|
|
49798
|
-
3. **Use --format json**: AI parsing of JSON is more reliable than tables
|
|
49799
|
-
4. **Check permission level**: Review \`.dbcli\` to understand what operations are allowed`;
|
|
49800
|
-
return `${frontmatter}
|
|
49801
|
-
|
|
49802
|
-
${header}
|
|
49803
|
-
|
|
49804
|
-
${commandDocs}
|
|
49805
|
-
|
|
49806
|
-
${footer}`;
|
|
49807
|
-
}
|
|
49808
|
-
renderCommandSection(cmd) {
|
|
49809
|
-
const argsStr = cmd.args.length > 0 ? " " + cmd.args.join(" ") : "";
|
|
49810
|
-
const optionsStr = cmd.options.length > 0 ? cmd.options.map((opt) => `- \`${opt.flag}\`: ${opt.description}`).join(`
|
|
49811
|
-
`) : "(No options)";
|
|
49812
|
-
const examplesStr = cmd.examples.map((ex) => `\`\`\`bash
|
|
49813
|
-
${ex}
|
|
49814
|
-
\`\`\``).join(`
|
|
49815
|
-
|
|
49816
|
-
`);
|
|
49817
|
-
return `### ${cmd.name}
|
|
49818
|
-
|
|
49819
|
-
${cmd.description}
|
|
49820
|
-
|
|
49821
|
-
**Usage:** \`dbcli ${cmd.name}${argsStr}\`
|
|
49822
|
-
|
|
49823
|
-
**Options:**
|
|
49824
|
-
${optionsStr}
|
|
49825
|
-
|
|
49826
|
-
**Permission required:** ${cmd.permissionLevel} or higher
|
|
49827
|
-
|
|
49828
|
-
**Example:**
|
|
49829
|
-
${examplesStr}`;
|
|
49830
|
-
}
|
|
49831
|
-
}
|
|
49832
|
-
|
|
49833
|
-
// src/commands/skill.ts
|
|
49834
|
-
async function skillCommand(program2, options) {
|
|
49900
|
+
var SKILL_SOURCE_PATH = path.resolve(import.meta.dir, "../../assets/SKILL.md");
|
|
49901
|
+
async function skillCommand(_program, options) {
|
|
49835
49902
|
try {
|
|
49836
|
-
const
|
|
49837
|
-
if (!
|
|
49838
|
-
throw new Error(
|
|
49903
|
+
const skillFile = Bun.file(SKILL_SOURCE_PATH);
|
|
49904
|
+
if (!await skillFile.exists()) {
|
|
49905
|
+
throw new Error(`Skill source not found: ${SKILL_SOURCE_PATH}`);
|
|
49839
49906
|
}
|
|
49840
|
-
const
|
|
49841
|
-
program: program2,
|
|
49842
|
-
config,
|
|
49843
|
-
permissionLevel: config.permission
|
|
49844
|
-
});
|
|
49845
|
-
const skillMarkdown = skillGen.generateSkillMarkdown();
|
|
49907
|
+
const skillMarkdown = await skillFile.text();
|
|
49846
49908
|
if (options.output) {
|
|
49847
49909
|
await Bun.file(options.output).write(skillMarkdown);
|
|
49848
49910
|
console.error(`Skill written to ${options.output}`);
|
|
@@ -50067,6 +50129,428 @@ columnCmd.command("remove <table.column>").description("Remove column from black
|
|
|
50067
50129
|
}
|
|
50068
50130
|
});
|
|
50069
50131
|
|
|
50132
|
+
// src/core/health-checker.ts
|
|
50133
|
+
init_size_category();
|
|
50134
|
+
|
|
50135
|
+
class HealthChecker {
|
|
50136
|
+
adapter;
|
|
50137
|
+
constructor(adapter) {
|
|
50138
|
+
this.adapter = adapter;
|
|
50139
|
+
}
|
|
50140
|
+
async check(schema, options = {}) {
|
|
50141
|
+
const checks = options.checks || ["nulls", "duplicates", "orphans", "emptyStrings", "rowCount"];
|
|
50142
|
+
const sample2 = options.sample || 1e4;
|
|
50143
|
+
const blacklisted = options.blacklistedColumns || new Set;
|
|
50144
|
+
const visibleColumns = schema.columns.filter((c) => !blacklisted.has(`${schema.name}.${c.name}`));
|
|
50145
|
+
const countResult = await this.adapter.execute(`SELECT COUNT(*) as count FROM \`${schema.name}\``);
|
|
50146
|
+
const rowCount = countResult[0]?.count || 0;
|
|
50147
|
+
const nulls = checks.includes("nulls") ? await this.checkNulls(schema.name, visibleColumns, rowCount, sample2) : [];
|
|
50148
|
+
const orphans = checks.includes("orphans") ? await this.checkOrphans(schema.name, visibleColumns) : [];
|
|
50149
|
+
const duplicates = checks.includes("duplicates") ? await this.checkDuplicates(schema.name, schema.indexes || []) : [];
|
|
50150
|
+
const emptyStrings = checks.includes("emptyStrings") ? await this.checkEmptyStrings(schema.name, visibleColumns) : [];
|
|
50151
|
+
const issues = orphans.length + duplicates.length;
|
|
50152
|
+
const warnings = nulls.filter((n) => n.nullPercent > 50).length + emptyStrings.length;
|
|
50153
|
+
const clean = Math.max(0, checks.length - (issues > 0 ? 1 : 0) - (warnings > 0 ? 1 : 0));
|
|
50154
|
+
return {
|
|
50155
|
+
table: schema.name,
|
|
50156
|
+
rowCount,
|
|
50157
|
+
sizeCategory: getSizeCategory(schema.estimatedRowCount),
|
|
50158
|
+
checks: { nulls, orphans, duplicates, emptyStrings },
|
|
50159
|
+
summary: { issues, warnings, clean },
|
|
50160
|
+
skippedColumns: blacklisted.size > 0 ? Array.from(blacklisted).filter((c) => c.startsWith(`${schema.name}.`)) : undefined
|
|
50161
|
+
};
|
|
50162
|
+
}
|
|
50163
|
+
async checkNulls(tableName, columns, totalRows, sample2) {
|
|
50164
|
+
if (totalRows === 0)
|
|
50165
|
+
return [];
|
|
50166
|
+
const nullableColumns = columns.filter((c) => c.nullable);
|
|
50167
|
+
const results = [];
|
|
50168
|
+
for (const col of nullableColumns) {
|
|
50169
|
+
try {
|
|
50170
|
+
const useSample = totalRows > sample2;
|
|
50171
|
+
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}\``;
|
|
50172
|
+
const result = await this.adapter.execute(sql);
|
|
50173
|
+
const nullCount = result[0]?.null_count || 0;
|
|
50174
|
+
if (nullCount > 0) {
|
|
50175
|
+
const sampleSize = Math.min(totalRows, sample2);
|
|
50176
|
+
results.push({
|
|
50177
|
+
column: col.name,
|
|
50178
|
+
nullCount,
|
|
50179
|
+
nullPercent: Number((nullCount / sampleSize * 100).toFixed(1))
|
|
50180
|
+
});
|
|
50181
|
+
}
|
|
50182
|
+
} catch {}
|
|
50183
|
+
}
|
|
50184
|
+
return results;
|
|
50185
|
+
}
|
|
50186
|
+
async checkOrphans(tableName, columns) {
|
|
50187
|
+
const fkColumns = columns.filter((c) => c.foreignKey);
|
|
50188
|
+
const results = [];
|
|
50189
|
+
for (const col of fkColumns) {
|
|
50190
|
+
if (!col.foreignKey)
|
|
50191
|
+
continue;
|
|
50192
|
+
try {
|
|
50193
|
+
const sql = `
|
|
50194
|
+
SELECT COUNT(*) as orphan_count
|
|
50195
|
+
FROM \`${tableName}\` child
|
|
50196
|
+
LEFT JOIN \`${col.foreignKey.table}\` parent
|
|
50197
|
+
ON child.\`${col.name}\` = parent.\`${col.foreignKey.column}\`
|
|
50198
|
+
WHERE child.\`${col.name}\` IS NOT NULL
|
|
50199
|
+
AND parent.\`${col.foreignKey.column}\` IS NULL
|
|
50200
|
+
`;
|
|
50201
|
+
const result = await this.adapter.execute(sql);
|
|
50202
|
+
const orphanCount = result[0]?.orphan_count || 0;
|
|
50203
|
+
if (orphanCount > 0) {
|
|
50204
|
+
results.push({
|
|
50205
|
+
column: col.name,
|
|
50206
|
+
references: `${col.foreignKey.table}.${col.foreignKey.column}`,
|
|
50207
|
+
orphanCount
|
|
50208
|
+
});
|
|
50209
|
+
}
|
|
50210
|
+
} catch {}
|
|
50211
|
+
}
|
|
50212
|
+
return results;
|
|
50213
|
+
}
|
|
50214
|
+
async checkDuplicates(tableName, indexes) {
|
|
50215
|
+
const uniqueIndexes = indexes.filter((idx) => idx.unique);
|
|
50216
|
+
const results = [];
|
|
50217
|
+
for (const idx of uniqueIndexes) {
|
|
50218
|
+
try {
|
|
50219
|
+
const colList = idx.columns.map((c) => `\`${c}\``).join(", ");
|
|
50220
|
+
const sql = `
|
|
50221
|
+
SELECT COUNT(*) as dup_count FROM (
|
|
50222
|
+
SELECT ${colList}
|
|
50223
|
+
FROM \`${tableName}\`
|
|
50224
|
+
GROUP BY ${colList}
|
|
50225
|
+
HAVING COUNT(*) > 1
|
|
50226
|
+
) dups
|
|
50227
|
+
`;
|
|
50228
|
+
const result = await this.adapter.execute(sql);
|
|
50229
|
+
const dupCount = result[0]?.dup_count || 0;
|
|
50230
|
+
if (dupCount > 0) {
|
|
50231
|
+
results.push({
|
|
50232
|
+
columns: idx.columns,
|
|
50233
|
+
indexName: idx.name,
|
|
50234
|
+
duplicateCount: dupCount
|
|
50235
|
+
});
|
|
50236
|
+
}
|
|
50237
|
+
} catch {}
|
|
50238
|
+
}
|
|
50239
|
+
return results;
|
|
50240
|
+
}
|
|
50241
|
+
async checkEmptyStrings(tableName, columns) {
|
|
50242
|
+
const stringColumns = columns.filter((c) => /varchar|text|char/i.test(c.type));
|
|
50243
|
+
const results = [];
|
|
50244
|
+
for (const col of stringColumns) {
|
|
50245
|
+
try {
|
|
50246
|
+
const sql = `SELECT COUNT(*) as empty_count FROM \`${tableName}\` WHERE \`${col.name}\` = ''`;
|
|
50247
|
+
const result = await this.adapter.execute(sql);
|
|
50248
|
+
const count = result[0]?.empty_count || 0;
|
|
50249
|
+
if (count > 0) {
|
|
50250
|
+
results.push({ column: col.name, count });
|
|
50251
|
+
}
|
|
50252
|
+
} catch {}
|
|
50253
|
+
}
|
|
50254
|
+
return results;
|
|
50255
|
+
}
|
|
50256
|
+
}
|
|
50257
|
+
|
|
50258
|
+
// src/commands/check.ts
|
|
50259
|
+
init_size_category();
|
|
50260
|
+
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);
|
|
50261
|
+
async function checkAction(table, options) {
|
|
50262
|
+
try {
|
|
50263
|
+
const config = await configModule.read(options.config);
|
|
50264
|
+
if (!config.connection) {
|
|
50265
|
+
console.error("Database not configured. Run: dbcli init");
|
|
50266
|
+
process.exit(1);
|
|
50267
|
+
}
|
|
50268
|
+
const adapter = AdapterFactory.createAdapter(config.connection);
|
|
50269
|
+
await adapter.connect();
|
|
50270
|
+
try {
|
|
50271
|
+
const checker = new HealthChecker(adapter);
|
|
50272
|
+
const blacklistManager = new BlacklistManager(config);
|
|
50273
|
+
const blacklistedColumns = getBlacklistedColumnSet(blacklistManager);
|
|
50274
|
+
const blacklistedTables = getBlacklistedTableSet(blacklistManager);
|
|
50275
|
+
const checkTypes = options.checks ? options.checks.split(",") : undefined;
|
|
50276
|
+
const sampleSize = parseInt(options.sample, 10) || 1e4;
|
|
50277
|
+
if (table) {
|
|
50278
|
+
if (blacklistedTables.has(table.toLowerCase())) {
|
|
50279
|
+
console.error(`Table "${table}" is blacklisted`);
|
|
50280
|
+
process.exit(1);
|
|
50281
|
+
}
|
|
50282
|
+
const schema = await adapter.getTableSchema(table);
|
|
50283
|
+
const report = await checker.check(schema, {
|
|
50284
|
+
checks: checkTypes,
|
|
50285
|
+
sample: sampleSize,
|
|
50286
|
+
blacklistedColumns
|
|
50287
|
+
});
|
|
50288
|
+
outputReport(report, options.format);
|
|
50289
|
+
} else if (options.all) {
|
|
50290
|
+
const tables = await adapter.listTables();
|
|
50291
|
+
const reports = [];
|
|
50292
|
+
const skipped = [];
|
|
50293
|
+
for (const t7 of tables) {
|
|
50294
|
+
if (blacklistedTables.has(t7.name.toLowerCase())) {
|
|
50295
|
+
skipped.push(`${t7.name} (blacklisted)`);
|
|
50296
|
+
continue;
|
|
50297
|
+
}
|
|
50298
|
+
if (t7.tableType === "view") {
|
|
50299
|
+
skipped.push(`${t7.name} (view)`);
|
|
50300
|
+
continue;
|
|
50301
|
+
}
|
|
50302
|
+
const category = getSizeCategory(t7.estimatedRowCount);
|
|
50303
|
+
if (category === "huge" && !options.includeLarge) {
|
|
50304
|
+
skipped.push(`${t7.name} (~${(t7.estimatedRowCount || 0).toLocaleString()} rows, huge)`);
|
|
50305
|
+
continue;
|
|
50306
|
+
}
|
|
50307
|
+
const schema = await adapter.getTableSchema(t7.name);
|
|
50308
|
+
const report = await checker.check(schema, {
|
|
50309
|
+
checks: checkTypes,
|
|
50310
|
+
sample: sampleSize,
|
|
50311
|
+
blacklistedColumns
|
|
50312
|
+
});
|
|
50313
|
+
reports.push(report);
|
|
50314
|
+
}
|
|
50315
|
+
if (options.format === "json") {
|
|
50316
|
+
console.log(JSON.stringify({ reports, skipped }, null, 2));
|
|
50317
|
+
} else {
|
|
50318
|
+
for (const report of reports) {
|
|
50319
|
+
outputReport(report, "table");
|
|
50320
|
+
console.log("");
|
|
50321
|
+
}
|
|
50322
|
+
if (skipped.length > 0) {
|
|
50323
|
+
console.log(`Skipped: ${skipped.join(", ")}`);
|
|
50324
|
+
}
|
|
50325
|
+
}
|
|
50326
|
+
} else {
|
|
50327
|
+
console.error("Specify a table name or use --all");
|
|
50328
|
+
process.exit(1);
|
|
50329
|
+
}
|
|
50330
|
+
} finally {
|
|
50331
|
+
await adapter.disconnect();
|
|
50332
|
+
}
|
|
50333
|
+
} catch (error) {
|
|
50334
|
+
if (error instanceof Error) {
|
|
50335
|
+
console.error(error.message);
|
|
50336
|
+
if (error instanceof ConnectionError) {
|
|
50337
|
+
error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
|
|
50338
|
+
}
|
|
50339
|
+
}
|
|
50340
|
+
process.exit(1);
|
|
50341
|
+
}
|
|
50342
|
+
}
|
|
50343
|
+
function outputReport(report, format) {
|
|
50344
|
+
if (format === "json") {
|
|
50345
|
+
console.log(JSON.stringify(report, null, 2));
|
|
50346
|
+
} else {
|
|
50347
|
+
console.log(`
|
|
50348
|
+
Table: ${report.table} (${report.rowCount} rows, ${report.sizeCategory})`);
|
|
50349
|
+
console.log(` Nulls: ${report.checks.nulls.length} columns with NULLs`);
|
|
50350
|
+
for (const n of report.checks.nulls) {
|
|
50351
|
+
console.log(` ${n.column}: ${n.nullCount} nulls (${n.nullPercent}%)`);
|
|
50352
|
+
}
|
|
50353
|
+
console.log(` Orphans: ${report.checks.orphans.length} FK violations`);
|
|
50354
|
+
for (const o of report.checks.orphans) {
|
|
50355
|
+
console.log(` ${o.column} -> ${o.references}: ${o.orphanCount} orphans`);
|
|
50356
|
+
}
|
|
50357
|
+
console.log(` Duplicates: ${report.checks.duplicates.length} unique index violations`);
|
|
50358
|
+
for (const d of report.checks.duplicates) {
|
|
50359
|
+
console.log(` ${d.indexName} (${d.columns.join(",")}): ${d.duplicateCount} duplicates`);
|
|
50360
|
+
}
|
|
50361
|
+
console.log(` Empty strings: ${report.checks.emptyStrings.length} columns`);
|
|
50362
|
+
for (const e of report.checks.emptyStrings) {
|
|
50363
|
+
console.log(` ${e.column}: ${e.count} empty strings`);
|
|
50364
|
+
}
|
|
50365
|
+
console.log(` Summary: ${report.summary.issues} issues, ${report.summary.warnings} warnings, ${report.summary.clean} clean`);
|
|
50366
|
+
}
|
|
50367
|
+
}
|
|
50368
|
+
function getBlacklistedColumnSet(manager) {
|
|
50369
|
+
const state = manager.state;
|
|
50370
|
+
const result = new Set;
|
|
50371
|
+
if (state?.columns) {
|
|
50372
|
+
for (const [table, cols] of state.columns.entries()) {
|
|
50373
|
+
for (const col of cols) {
|
|
50374
|
+
result.add(`${table}.${col}`);
|
|
50375
|
+
}
|
|
50376
|
+
}
|
|
50377
|
+
}
|
|
50378
|
+
return result;
|
|
50379
|
+
}
|
|
50380
|
+
function getBlacklistedTableSet(manager) {
|
|
50381
|
+
const state = manager.state;
|
|
50382
|
+
return state?.tables || new Set;
|
|
50383
|
+
}
|
|
50384
|
+
|
|
50385
|
+
// src/commands/diff.ts
|
|
50386
|
+
function compareSnapshots(before, after) {
|
|
50387
|
+
const beforeTables = new Set(Object.keys(before.tables));
|
|
50388
|
+
const afterTables = new Set(Object.keys(after.tables));
|
|
50389
|
+
const addedTables = Array.from(afterTables).filter((t7) => !beforeTables.has(t7));
|
|
50390
|
+
const removedTables = Array.from(beforeTables).filter((t7) => !afterTables.has(t7));
|
|
50391
|
+
const addedColumns = [];
|
|
50392
|
+
const removedColumns = [];
|
|
50393
|
+
const modifiedColumns = [];
|
|
50394
|
+
const indexChanges = [];
|
|
50395
|
+
for (const tableName of addedTables) {
|
|
50396
|
+
for (const col of after.tables[tableName].columns) {
|
|
50397
|
+
addedColumns.push({ table: tableName, column: col.name, type: col.type, nullable: col.nullable });
|
|
50398
|
+
}
|
|
50399
|
+
}
|
|
50400
|
+
for (const tableName of removedTables) {
|
|
50401
|
+
for (const col of before.tables[tableName].columns) {
|
|
50402
|
+
removedColumns.push({ table: tableName, column: col.name, type: col.type });
|
|
50403
|
+
}
|
|
50404
|
+
}
|
|
50405
|
+
const commonTables = Array.from(afterTables).filter((t7) => beforeTables.has(t7));
|
|
50406
|
+
for (const tableName of commonTables) {
|
|
50407
|
+
const beforeTable = before.tables[tableName];
|
|
50408
|
+
const afterTable = after.tables[tableName];
|
|
50409
|
+
const beforeColMap = new Map(beforeTable.columns.map((c) => [c.name, c]));
|
|
50410
|
+
const afterColMap = new Map(afterTable.columns.map((c) => [c.name, c]));
|
|
50411
|
+
for (const [name, col] of afterColMap) {
|
|
50412
|
+
if (!beforeColMap.has(name)) {
|
|
50413
|
+
addedColumns.push({ table: tableName, column: name, type: col.type, nullable: col.nullable });
|
|
50414
|
+
}
|
|
50415
|
+
}
|
|
50416
|
+
for (const [name, col] of beforeColMap) {
|
|
50417
|
+
if (!afterColMap.has(name)) {
|
|
50418
|
+
removedColumns.push({ table: tableName, column: name, type: col.type });
|
|
50419
|
+
}
|
|
50420
|
+
}
|
|
50421
|
+
for (const [name, afterCol] of afterColMap) {
|
|
50422
|
+
const beforeCol = beforeColMap.get(name);
|
|
50423
|
+
if (!beforeCol)
|
|
50424
|
+
continue;
|
|
50425
|
+
if (beforeCol.type.toLowerCase() !== afterCol.type.toLowerCase() || beforeCol.nullable !== afterCol.nullable) {
|
|
50426
|
+
modifiedColumns.push({
|
|
50427
|
+
table: tableName,
|
|
50428
|
+
column: name,
|
|
50429
|
+
before: { type: beforeCol.type, nullable: beforeCol.nullable },
|
|
50430
|
+
after: { type: afterCol.type, nullable: afterCol.nullable }
|
|
50431
|
+
});
|
|
50432
|
+
}
|
|
50433
|
+
}
|
|
50434
|
+
const beforeIndexes = new Map((beforeTable.indexes || []).map((i) => [i.name, i]));
|
|
50435
|
+
const afterIndexes = new Map((afterTable.indexes || []).map((i) => [i.name, i]));
|
|
50436
|
+
for (const name of afterIndexes.keys()) {
|
|
50437
|
+
if (!beforeIndexes.has(name)) {
|
|
50438
|
+
indexChanges.push({ table: tableName, name, change: "added" });
|
|
50439
|
+
}
|
|
50440
|
+
}
|
|
50441
|
+
for (const name of beforeIndexes.keys()) {
|
|
50442
|
+
if (!afterIndexes.has(name)) {
|
|
50443
|
+
indexChanges.push({ table: tableName, name, change: "removed" });
|
|
50444
|
+
}
|
|
50445
|
+
}
|
|
50446
|
+
}
|
|
50447
|
+
return {
|
|
50448
|
+
added: { tables: addedTables, columns: addedColumns },
|
|
50449
|
+
removed: { tables: removedTables, columns: removedColumns },
|
|
50450
|
+
modified: { columns: modifiedColumns, indexes: indexChanges },
|
|
50451
|
+
summary: {
|
|
50452
|
+
added: addedTables.length + addedColumns.length,
|
|
50453
|
+
removed: removedTables.length + removedColumns.length,
|
|
50454
|
+
modified: modifiedColumns.length + indexChanges.length
|
|
50455
|
+
}
|
|
50456
|
+
};
|
|
50457
|
+
}
|
|
50458
|
+
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);
|
|
50459
|
+
async function diffAction(options) {
|
|
50460
|
+
try {
|
|
50461
|
+
if (!options.snapshot && !options.against) {
|
|
50462
|
+
console.error("Specify --snapshot <path> to save, or --against <path> to compare");
|
|
50463
|
+
process.exit(1);
|
|
50464
|
+
}
|
|
50465
|
+
const config = await configModule.read(options.config);
|
|
50466
|
+
if (!config.connection) {
|
|
50467
|
+
console.error("Database not configured. Run: dbcli init");
|
|
50468
|
+
process.exit(1);
|
|
50469
|
+
}
|
|
50470
|
+
const adapter = AdapterFactory.createAdapter(config.connection);
|
|
50471
|
+
await adapter.connect();
|
|
50472
|
+
try {
|
|
50473
|
+
const tables = await adapter.listTables();
|
|
50474
|
+
const currentSnapshot = {
|
|
50475
|
+
tables: {},
|
|
50476
|
+
createdAt: new Date().toISOString()
|
|
50477
|
+
};
|
|
50478
|
+
for (const t7 of tables) {
|
|
50479
|
+
if (t7.tableType === "view")
|
|
50480
|
+
continue;
|
|
50481
|
+
const schema = await adapter.getTableSchema(t7.name);
|
|
50482
|
+
currentSnapshot.tables[t7.name] = {
|
|
50483
|
+
name: schema.name,
|
|
50484
|
+
columns: schema.columns,
|
|
50485
|
+
indexes: schema.indexes || []
|
|
50486
|
+
};
|
|
50487
|
+
}
|
|
50488
|
+
if (options.snapshot) {
|
|
50489
|
+
await Bun.write(options.snapshot, JSON.stringify(currentSnapshot, null, 2));
|
|
50490
|
+
console.error(`Snapshot saved to ${options.snapshot} (${Object.keys(currentSnapshot.tables).length} tables)`);
|
|
50491
|
+
return;
|
|
50492
|
+
}
|
|
50493
|
+
if (options.against) {
|
|
50494
|
+
const beforeFile = Bun.file(options.against);
|
|
50495
|
+
if (!await beforeFile.exists()) {
|
|
50496
|
+
console.error(`Snapshot file not found: ${options.against}`);
|
|
50497
|
+
process.exit(1);
|
|
50498
|
+
}
|
|
50499
|
+
const beforeSnapshot = JSON.parse(await beforeFile.text());
|
|
50500
|
+
const result = compareSnapshots(beforeSnapshot, currentSnapshot);
|
|
50501
|
+
if (options.format === "json") {
|
|
50502
|
+
console.log(JSON.stringify(result, null, 2));
|
|
50503
|
+
} else {
|
|
50504
|
+
console.log(`
|
|
50505
|
+
Schema diff (${beforeSnapshot.createdAt} -> ${currentSnapshot.createdAt}):`);
|
|
50506
|
+
if (result.added.tables.length > 0)
|
|
50507
|
+
console.log(`
|
|
50508
|
+
Added tables: ${result.added.tables.join(", ")}`);
|
|
50509
|
+
if (result.removed.tables.length > 0)
|
|
50510
|
+
console.log(`
|
|
50511
|
+
Removed tables: ${result.removed.tables.join(", ")}`);
|
|
50512
|
+
if (result.added.columns.length > 0) {
|
|
50513
|
+
console.log(`
|
|
50514
|
+
Added columns:`);
|
|
50515
|
+
for (const c of result.added.columns)
|
|
50516
|
+
console.log(` ${c.table}.${c.column} (${c.type})`);
|
|
50517
|
+
}
|
|
50518
|
+
if (result.removed.columns.length > 0) {
|
|
50519
|
+
console.log(`
|
|
50520
|
+
Removed columns:`);
|
|
50521
|
+
for (const c of result.removed.columns)
|
|
50522
|
+
console.log(` ${c.table}.${c.column} (${c.type})`);
|
|
50523
|
+
}
|
|
50524
|
+
if (result.modified.columns.length > 0) {
|
|
50525
|
+
console.log(`
|
|
50526
|
+
Modified columns:`);
|
|
50527
|
+
for (const c of result.modified.columns)
|
|
50528
|
+
console.log(` ${c.table}.${c.column}: ${c.before.type} -> ${c.after.type}`);
|
|
50529
|
+
}
|
|
50530
|
+
if (result.modified.indexes.length > 0) {
|
|
50531
|
+
console.log(`
|
|
50532
|
+
Index changes:`);
|
|
50533
|
+
for (const i of result.modified.indexes)
|
|
50534
|
+
console.log(` ${i.table}.${i.name}: ${i.change}`);
|
|
50535
|
+
}
|
|
50536
|
+
console.log(`
|
|
50537
|
+
Summary: +${result.summary.added} -${result.summary.removed} ~${result.summary.modified}`);
|
|
50538
|
+
}
|
|
50539
|
+
}
|
|
50540
|
+
} finally {
|
|
50541
|
+
await adapter.disconnect();
|
|
50542
|
+
}
|
|
50543
|
+
} catch (error) {
|
|
50544
|
+
if (error instanceof Error) {
|
|
50545
|
+
console.error(error.message);
|
|
50546
|
+
if (error instanceof ConnectionError) {
|
|
50547
|
+
error.hints.forEach((hint) => console.error(` Hint: ${hint}`));
|
|
50548
|
+
}
|
|
50549
|
+
}
|
|
50550
|
+
process.exit(1);
|
|
50551
|
+
}
|
|
50552
|
+
}
|
|
50553
|
+
|
|
50070
50554
|
// src/cli.ts
|
|
50071
50555
|
var program2 = new Command().name("dbcli").description("Database CLI for AI agents").version(package_default.version).option("--config <path>", "Path to .dbcli config file", ".dbcli");
|
|
50072
50556
|
program2.addCommand(initCommand);
|
|
@@ -50125,6 +50609,8 @@ program2.command("skill").description(t("skill.description")).option("--install
|
|
|
50125
50609
|
}
|
|
50126
50610
|
});
|
|
50127
50611
|
program2.addCommand(blacklistCommand);
|
|
50612
|
+
program2.addCommand(checkCommand);
|
|
50613
|
+
program2.addCommand(diffCommand);
|
|
50128
50614
|
if (!process.argv.slice(2).length) {
|
|
50129
50615
|
program2.outputHelp();
|
|
50130
50616
|
}
|