@carllee1983/dbcli 1.51.2 → 1.52.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/.cursor/rules/dbcli.mdc +2 -2
- package/.cursor/skills/dbcli/reference.md +170 -1
- package/.github/skills/dbcli/SKILL.md +2 -2
- package/.github/skills/dbcli/reference.md +170 -1
- package/CHANGELOG.md +17 -0
- package/README.md +1 -0
- package/README.zh-TW.md +1 -0
- package/assets/SKILL.md +2 -2
- package/assets/reference.md +170 -1
- package/assets/tasks/design-review.md +42 -0
- package/dist/cli.mjs +790 -35
- package/dist/core.d.ts +8 -0
- package/package.json +4 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +2 -2
- package/plugins/dbcli-agent/skills/dbcli/reference.md +170 -1
- package/skills/dbcli/SKILL.md +2 -2
- package/skills/dbcli/reference.md +170 -1
package/dist/cli.mjs
CHANGED
|
@@ -52,7 +52,7 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "@carllee1983/dbcli",
|
|
55
|
-
version: "1.
|
|
55
|
+
version: "1.52.0",
|
|
56
56
|
description: "Database CLI for AI agents",
|
|
57
57
|
type: "module",
|
|
58
58
|
publishConfig: {
|
|
@@ -124,6 +124,7 @@ var init_package = __esm(() => {
|
|
|
124
124
|
test: "bun test --timeout 30000",
|
|
125
125
|
"test:unit": "bun test tests/unit tests/core",
|
|
126
126
|
"test:integration": "bun test tests/integration",
|
|
127
|
+
"test:gherkin": "bun test tests/gherkin",
|
|
127
128
|
"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",
|
|
128
129
|
"docs:check": "bun run scripts/check-user-docs.ts",
|
|
129
130
|
"contract:check": "bun run scripts/check-cli-contract.ts",
|
|
@@ -154,6 +155,8 @@ var init_package = __esm(() => {
|
|
|
154
155
|
zod: "^3.25.76"
|
|
155
156
|
},
|
|
156
157
|
devDependencies: {
|
|
158
|
+
"@cucumber/gherkin": "^42.0.1",
|
|
159
|
+
"@cucumber/messages": "^34.2.1",
|
|
157
160
|
"@eslint/js": "^9.39.4",
|
|
158
161
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
159
162
|
"@inquirer/prompts": "^8.4.3",
|
|
@@ -80459,6 +80462,9 @@ class QueryResultFormatter {
|
|
|
80459
80462
|
if (result.executionTimeMs !== undefined) {
|
|
80460
80463
|
footerLines.push(`Execution time: ${result.executionTimeMs}ms`);
|
|
80461
80464
|
}
|
|
80465
|
+
if (result.metadata?.performanceAdvisory) {
|
|
80466
|
+
footerLines.push(`Performance hint: ${result.metadata.performanceAdvisory.recommendation}`);
|
|
80467
|
+
}
|
|
80462
80468
|
return footerLines.join(" | ");
|
|
80463
80469
|
}
|
|
80464
80470
|
formatCSV(result) {
|
|
@@ -86348,6 +86354,63 @@ var init_query_fanout = __esm(() => {
|
|
|
86348
86354
|
init_permission_guard();
|
|
86349
86355
|
});
|
|
86350
86356
|
|
|
86357
|
+
// src/core/slow-query-advisory.ts
|
|
86358
|
+
function slowQueryThresholdFor(options) {
|
|
86359
|
+
if (options.recovery === true)
|
|
86360
|
+
return 0;
|
|
86361
|
+
const threshold = options.slowMs ?? DEFAULT_SLOW_QUERY_MS;
|
|
86362
|
+
assertValidSlowQueryThreshold(threshold);
|
|
86363
|
+
return threshold;
|
|
86364
|
+
}
|
|
86365
|
+
function assertValidSlowQueryThreshold(value) {
|
|
86366
|
+
if (value === undefined)
|
|
86367
|
+
return;
|
|
86368
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
86369
|
+
throw new Error("slow-ms must be a non-negative integer");
|
|
86370
|
+
}
|
|
86371
|
+
}
|
|
86372
|
+
function parseSlowQueryThreshold(value) {
|
|
86373
|
+
if (!/^\d+$/.test(value)) {
|
|
86374
|
+
throw new Error("slow-ms must be a non-negative integer");
|
|
86375
|
+
}
|
|
86376
|
+
const parsed = Number(value);
|
|
86377
|
+
assertValidSlowQueryThreshold(parsed);
|
|
86378
|
+
return parsed;
|
|
86379
|
+
}
|
|
86380
|
+
function recommendationFor(system) {
|
|
86381
|
+
const covered = system === undefined || GUIDE_SLOW_QUERY_SYSTEMS.includes(system);
|
|
86382
|
+
return covered ? `Review safely with: dbcli guide slow-query --format markdown. ${NO_EXTRA_WORK}` : `Re-check the query shape and the filters it scans; dbcli ships no slow-query diagnostics for ${system}. ${NO_EXTRA_WORK}`;
|
|
86383
|
+
}
|
|
86384
|
+
function buildSlowQueryAdvisory(executionTimeMs, options) {
|
|
86385
|
+
const thresholdMs = slowQueryThresholdFor(options);
|
|
86386
|
+
if (thresholdMs === 0 || executionTimeMs === undefined || executionTimeMs < thresholdMs) {
|
|
86387
|
+
return;
|
|
86388
|
+
}
|
|
86389
|
+
return {
|
|
86390
|
+
code: "SLOW_QUERY",
|
|
86391
|
+
executionTimeMs,
|
|
86392
|
+
thresholdMs,
|
|
86393
|
+
recommendation: recommendationFor(options.system)
|
|
86394
|
+
};
|
|
86395
|
+
}
|
|
86396
|
+
function attachSlowQueryAdvisory(result, options) {
|
|
86397
|
+
const advisory = buildSlowQueryAdvisory(result.executionTimeMs, options);
|
|
86398
|
+
if (!advisory)
|
|
86399
|
+
return result;
|
|
86400
|
+
return {
|
|
86401
|
+
...result,
|
|
86402
|
+
metadata: {
|
|
86403
|
+
statement: result.metadata?.statement ?? "UNKNOWN",
|
|
86404
|
+
...result.metadata,
|
|
86405
|
+
performanceAdvisory: advisory
|
|
86406
|
+
}
|
|
86407
|
+
};
|
|
86408
|
+
}
|
|
86409
|
+
var DEFAULT_SLOW_QUERY_MS = 1000, GUIDE_SLOW_QUERY_SYSTEMS, NO_EXTRA_WORK = "This hint runs no additional database diagnostics.";
|
|
86410
|
+
var init_slow_query_advisory = __esm(() => {
|
|
86411
|
+
GUIDE_SLOW_QUERY_SYSTEMS = ["mariadb", "mysql", "postgresql", "redis"];
|
|
86412
|
+
});
|
|
86413
|
+
|
|
86351
86414
|
// src/commands/query-size-guard.ts
|
|
86352
86415
|
var exports_query_size_guard = {};
|
|
86353
86416
|
__export(exports_query_size_guard, {
|
|
@@ -86409,6 +86472,7 @@ async function queryCommand(sql, options, command) {
|
|
|
86409
86472
|
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
|
|
86410
86473
|
throw new Error("--limit must be a positive integer");
|
|
86411
86474
|
}
|
|
86475
|
+
assertValidSlowQueryThreshold(options.slowMs);
|
|
86412
86476
|
if (options.format) {
|
|
86413
86477
|
validateFormat(options.format, [...ALLOWED_FORMATS3, "html"], "query");
|
|
86414
86478
|
}
|
|
@@ -86628,6 +86692,14 @@ async function executeConnectionQuery(query, options, context, fieldSelection) {
|
|
|
86628
86692
|
} else {
|
|
86629
86693
|
execution = await sqlQueryBranch(query, options, context, fieldSelection);
|
|
86630
86694
|
}
|
|
86695
|
+
execution = {
|
|
86696
|
+
...execution,
|
|
86697
|
+
result: attachSlowQueryAdvisory(execution.result, {
|
|
86698
|
+
slowMs: options.slowMs,
|
|
86699
|
+
recovery: options.recovery,
|
|
86700
|
+
system
|
|
86701
|
+
})
|
|
86702
|
+
};
|
|
86631
86703
|
await writeAuditEntry(config, "query", auditOptions, {
|
|
86632
86704
|
success: true,
|
|
86633
86705
|
sql: query,
|
|
@@ -86936,6 +87008,7 @@ var init_query = __esm(() => {
|
|
|
86936
87008
|
init_connection_selector();
|
|
86937
87009
|
init_query_fanout();
|
|
86938
87010
|
init_permission_guard();
|
|
87011
|
+
init_slow_query_advisory();
|
|
86939
87012
|
ALLOWED_FORMATS3 = ["table", "json", "csv"];
|
|
86940
87013
|
});
|
|
86941
87014
|
|
|
@@ -87527,14 +87600,14 @@ async function qMongoBranch(snippet, prepared, options, config) {
|
|
|
87527
87600
|
} else {
|
|
87528
87601
|
const columnNames = masked[0] ? Object.keys(masked[0]) : [];
|
|
87529
87602
|
const formatter = new QueryResultFormatter;
|
|
87530
|
-
console.log(formatter.format({
|
|
87603
|
+
console.log(formatter.format(attachSlowQueryAdvisory({
|
|
87531
87604
|
rows: masked,
|
|
87532
87605
|
rowCount: masked.length,
|
|
87533
87606
|
columnNames,
|
|
87534
87607
|
columnTypes: [],
|
|
87535
87608
|
executionTimeMs,
|
|
87536
87609
|
metadata: { statement: "SELECT", affectedRows: 0 }
|
|
87537
|
-
}, { format: options.format ?? "table" }));
|
|
87610
|
+
}, { slowMs: options.slowMs, recovery: options.recovery, system: "mongodb" }), { format: options.format ?? "table" }));
|
|
87538
87611
|
}
|
|
87539
87612
|
await writeAuditEntry(config, "q", options, {
|
|
87540
87613
|
success: true,
|
|
@@ -87555,6 +87628,7 @@ var init_q_mongo = __esm(() => {
|
|
|
87555
87628
|
init_html_formatter();
|
|
87556
87629
|
init_opener();
|
|
87557
87630
|
init_integration_helper();
|
|
87631
|
+
init_slow_query_advisory();
|
|
87558
87632
|
});
|
|
87559
87633
|
|
|
87560
87634
|
// src/commands/q.ts
|
|
@@ -87591,6 +87665,7 @@ async function qCommand(name2, options, command) {
|
|
|
87591
87665
|
let config;
|
|
87592
87666
|
let targetNameForAudit = name2;
|
|
87593
87667
|
try {
|
|
87668
|
+
assertValidSlowQueryThreshold(options.slowMs);
|
|
87594
87669
|
if (!name2?.startsWith("@")) {
|
|
87595
87670
|
throw new Error(`Snippet name must start with '@' (got '${name2}')`);
|
|
87596
87671
|
}
|
|
@@ -87686,7 +87761,7 @@ async function qCommand(name2, options, command) {
|
|
|
87686
87761
|
return;
|
|
87687
87762
|
}
|
|
87688
87763
|
const formatter = new QueryResultFormatter;
|
|
87689
|
-
const out = formatter.format({
|
|
87764
|
+
const out = formatter.format(attachSlowQueryAdvisory({
|
|
87690
87765
|
rows: filtered.filteredRows,
|
|
87691
87766
|
rowCount: filtered.filteredRows.length,
|
|
87692
87767
|
columnNames: columnNames.filter((c2) => !filtered.omittedColumns.includes(c2)),
|
|
@@ -87698,7 +87773,7 @@ async function qCommand(name2, options, command) {
|
|
|
87698
87773
|
...securityNotification ? { securityNotification } : {}
|
|
87699
87774
|
},
|
|
87700
87775
|
...limitedResult ? { appliedLimit: limitedResult.metadata } : {}
|
|
87701
|
-
}, { format: options.format ?? "table" });
|
|
87776
|
+
}, { slowMs: options.slowMs, recovery: options.recovery, system: connectionSystem }), { format: options.format ?? "table" });
|
|
87702
87777
|
console.log(out);
|
|
87703
87778
|
await writeAuditEntry(config, "q", options, {
|
|
87704
87779
|
success: true,
|
|
@@ -87915,6 +87990,7 @@ var init_q = __esm(() => {
|
|
|
87915
87990
|
init_applied_limit();
|
|
87916
87991
|
init_cli_error();
|
|
87917
87992
|
init_strategies();
|
|
87993
|
+
init_slow_query_advisory();
|
|
87918
87994
|
});
|
|
87919
87995
|
|
|
87920
87996
|
// src/core/saved-queries/fold.ts
|
|
@@ -93917,7 +93993,7 @@ var init_normalized_schema = __esm(() => {
|
|
|
93917
93993
|
}))
|
|
93918
93994
|
});
|
|
93919
93995
|
normalizedSchemaZod = exports_external.object({
|
|
93920
|
-
source: exports_external.enum(["db", "prisma", "ddl", "json", "drizzle", "typeorm", "sequelize"]),
|
|
93996
|
+
source: exports_external.enum(["db", "design", "prisma", "ddl", "json", "drizzle", "typeorm", "sequelize"]),
|
|
93921
93997
|
defaultSchema: exports_external.string().min(1).optional(),
|
|
93922
93998
|
tables: exports_external.array(tableZod),
|
|
93923
93999
|
unparsed: exports_external.array(exports_external.object({ location: exports_external.string(), reason: exports_external.string().startsWith("blocked:") }))
|
|
@@ -94059,6 +94135,7 @@ function compareNormalized(orm, db, opts) {
|
|
|
94059
94135
|
const extraDefaultIgnore = opts.extraDefaultIgnore ?? [];
|
|
94060
94136
|
const defaultIgnore = [...DEFAULT_IGNORE, ...extraDefaultIgnore];
|
|
94061
94137
|
const ignorePatterns = opts.ignore.map(globToRegex2);
|
|
94138
|
+
const targetLabel = opts.targetLabel ?? "database";
|
|
94062
94139
|
const entries = [];
|
|
94063
94140
|
for (const tableKey of tableKeys) {
|
|
94064
94141
|
const ormTable = ormTables.get(tableKey);
|
|
@@ -94084,7 +94161,7 @@ function compareNormalized(orm, db, opts) {
|
|
|
94084
94161
|
severity: "error",
|
|
94085
94162
|
table,
|
|
94086
94163
|
object: "table",
|
|
94087
|
-
detail: `table '${table}' is defined in ${orm.source} but absent in the
|
|
94164
|
+
detail: `table '${table}' is defined in ${orm.source} but absent in the ${targetLabel}`
|
|
94088
94165
|
}));
|
|
94089
94166
|
continue;
|
|
94090
94167
|
}
|
|
@@ -94094,12 +94171,12 @@ function compareNormalized(orm, db, opts) {
|
|
|
94094
94171
|
severity: "warn",
|
|
94095
94172
|
table,
|
|
94096
94173
|
object: "table",
|
|
94097
|
-
detail: `table '${table}' exists in the
|
|
94174
|
+
detail: `table '${table}' exists in the ${targetLabel} but is not defined in ${orm.source}`
|
|
94098
94175
|
}));
|
|
94099
94176
|
continue;
|
|
94100
94177
|
}
|
|
94101
94178
|
if (ormTable && dbTable)
|
|
94102
|
-
compareTable(ormTable, dbTable, table, orm.source, entries);
|
|
94179
|
+
compareTable(ormTable, dbTable, table, orm.source, targetLabel, entries);
|
|
94103
94180
|
}
|
|
94104
94181
|
entries.sort(entryOrder);
|
|
94105
94182
|
const summary = {
|
|
@@ -94115,7 +94192,7 @@ function compareNormalized(orm, db, opts) {
|
|
|
94115
94192
|
summary
|
|
94116
94193
|
};
|
|
94117
94194
|
}
|
|
94118
|
-
function compareTable(ormTable, dbTable, table, ormSource, entries) {
|
|
94195
|
+
function compareTable(ormTable, dbTable, table, ormSource, targetLabel, entries) {
|
|
94119
94196
|
const ormColumns = new Map(ormTable.columns.map((column) => [column.name.toLowerCase(), column]));
|
|
94120
94197
|
const dbColumns = new Map(dbTable.columns.map((column) => [column.name.toLowerCase(), column]));
|
|
94121
94198
|
for (const [columnKey, ormColumn] of ormColumns) {
|
|
@@ -94126,7 +94203,7 @@ function compareTable(ormTable, dbTable, table, ormSource, entries) {
|
|
|
94126
94203
|
severity: "error",
|
|
94127
94204
|
table,
|
|
94128
94205
|
object: ormColumn.name,
|
|
94129
|
-
detail: `column '${ormColumn.name}' (${ormColumn.type}) is defined in ${ormSource} but absent in the
|
|
94206
|
+
detail: `column '${ormColumn.name}' (${ormColumn.type}) is defined in ${ormSource} but absent in the ${targetLabel}`
|
|
94130
94207
|
}, { kind: "column", table: ormTable.identity, column: ormColumn }));
|
|
94131
94208
|
continue;
|
|
94132
94209
|
}
|
|
@@ -94149,10 +94226,44 @@ function compareTable(ormTable, dbTable, table, ormSource, entries) {
|
|
|
94149
94226
|
severity: "warn",
|
|
94150
94227
|
table,
|
|
94151
94228
|
object: dbColumn.name,
|
|
94152
|
-
detail: `column '${dbColumn.name}' (${dbColumn.type}) exists in the
|
|
94229
|
+
detail: `column '${dbColumn.name}' (${dbColumn.type}) exists in the ${targetLabel} but is not defined in ${ormSource}`
|
|
94153
94230
|
}));
|
|
94154
94231
|
}
|
|
94155
|
-
compareIndexes(ormTable.indexes, dbTable.indexes, ormTable.identity, table, ormSource, entries);
|
|
94232
|
+
compareIndexes(ormTable.indexes, dbTable.indexes, ormTable.identity, table, ormSource, targetLabel, entries);
|
|
94233
|
+
compareForeignKeys(ormTable.foreignKeys, dbTable.foreignKeys, table, ormSource, targetLabel, entries);
|
|
94234
|
+
}
|
|
94235
|
+
function compareForeignKeys(ormForeignKeys, dbForeignKeys, table, ormSource, targetLabel, entries) {
|
|
94236
|
+
const ormByKey = new Map(ormForeignKeys.map((foreignKey) => [foreignKeyKey(foreignKey), foreignKey]));
|
|
94237
|
+
const dbByKey = new Map(dbForeignKeys.map((foreignKey) => [foreignKeyKey(foreignKey), foreignKey]));
|
|
94238
|
+
for (const [key, foreignKey] of ormByKey) {
|
|
94239
|
+
if (dbByKey.has(key))
|
|
94240
|
+
continue;
|
|
94241
|
+
entries.push(entryWithProposals({
|
|
94242
|
+
category: "missing_in_db",
|
|
94243
|
+
severity: "error",
|
|
94244
|
+
table,
|
|
94245
|
+
object: `foreign key (${foreignKey.columns.join(", ")})`,
|
|
94246
|
+
detail: `foreign key (${foreignKey.columns.join(", ")}) \u2192 ${qualifiedTableName(foreignKey.refTable)}(${foreignKey.refColumns.join(", ")}) is defined in ${ormSource} but absent in the ${targetLabel}`
|
|
94247
|
+
}));
|
|
94248
|
+
}
|
|
94249
|
+
for (const [key, foreignKey] of dbByKey) {
|
|
94250
|
+
if (ormByKey.has(key))
|
|
94251
|
+
continue;
|
|
94252
|
+
entries.push(entryWithProposals({
|
|
94253
|
+
category: "missing_in_orm",
|
|
94254
|
+
severity: "warn",
|
|
94255
|
+
table,
|
|
94256
|
+
object: `foreign key (${foreignKey.columns.join(", ")})`,
|
|
94257
|
+
detail: `foreign key (${foreignKey.columns.join(", ")}) \u2192 ${qualifiedTableName(foreignKey.refTable)}(${foreignKey.refColumns.join(", ")}) exists in the ${targetLabel} but is not defined in ${ormSource}`
|
|
94258
|
+
}));
|
|
94259
|
+
}
|
|
94260
|
+
}
|
|
94261
|
+
function foreignKeyKey(foreignKey) {
|
|
94262
|
+
return [
|
|
94263
|
+
foreignKey.columns.join("\x00"),
|
|
94264
|
+
tableIdentityKey(foreignKey.refTable),
|
|
94265
|
+
foreignKey.refColumns.join("\x00")
|
|
94266
|
+
].join("\x01");
|
|
94156
94267
|
}
|
|
94157
94268
|
function columnMismatch(ormColumn, dbColumn) {
|
|
94158
94269
|
const reasons = [];
|
|
@@ -94180,7 +94291,7 @@ function columnMismatch(ormColumn, dbColumn) {
|
|
|
94180
94291
|
function displayOptional(value) {
|
|
94181
94292
|
return value === undefined ? "none" : value;
|
|
94182
94293
|
}
|
|
94183
|
-
function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource, entries) {
|
|
94294
|
+
function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource, targetLabel, entries) {
|
|
94184
94295
|
const indexKey = (index) => JSON.stringify([index.columns.map((column) => column.toLowerCase()), index.unique]);
|
|
94185
94296
|
const dbKeys = new Set(dbIndexes.map(indexKey));
|
|
94186
94297
|
const ormKeys = new Set(ormIndexes.map(indexKey));
|
|
@@ -94196,7 +94307,7 @@ function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource,
|
|
|
94196
94307
|
severity: "error",
|
|
94197
94308
|
table,
|
|
94198
94309
|
object: `index(${index.columns.join(",")})`,
|
|
94199
|
-
detail: `${index.unique ? "unique " : ""}index on (${index.columns.join(", ")}) is defined in ${ormSource} but absent in the
|
|
94310
|
+
detail: `${index.unique ? "unique " : ""}index on (${index.columns.join(", ")}) is defined in ${ormSource} but absent in the ${targetLabel}`
|
|
94200
94311
|
}, { kind: "index", table: tableIdentity, index }));
|
|
94201
94312
|
}
|
|
94202
94313
|
for (const index of dbIndexes) {
|
|
@@ -94209,7 +94320,7 @@ function compareIndexes(ormIndexes, dbIndexes, tableIdentity, table, ormSource,
|
|
|
94209
94320
|
severity: "warn",
|
|
94210
94321
|
table,
|
|
94211
94322
|
object: `index(${index.columns.join(",")})`,
|
|
94212
|
-
detail: `${index.unique ? "unique " : ""}index on (${index.columns.join(", ")}) exists in the
|
|
94323
|
+
detail: `${index.unique ? "unique " : ""}index on (${index.columns.join(", ")}) exists in the ${targetLabel} but is not defined in ${ormSource}`
|
|
94213
94324
|
}, { kind: "index", table: tableIdentity, index }));
|
|
94214
94325
|
}
|
|
94215
94326
|
}
|
|
@@ -94417,15 +94528,7 @@ function mergeNormalizedSchemas(schemas) {
|
|
|
94417
94528
|
unparsed: schemas.flatMap((schema) => schema.unparsed)
|
|
94418
94529
|
});
|
|
94419
94530
|
}
|
|
94420
|
-
async function
|
|
94421
|
-
const system = config.connection?.system;
|
|
94422
|
-
if (!system || !["postgresql", "mysql", "mariadb"].includes(system)) {
|
|
94423
|
-
throw new Error(`This command requires a SQL connection, got: ${system ?? "none"}`);
|
|
94424
|
-
}
|
|
94425
|
-
const cached2 = config.schema ?? {};
|
|
94426
|
-
if (Object.keys(cached2).length === 0) {
|
|
94427
|
-
throw new Error("Schema cache is empty. Run 'dbcli schema' first.");
|
|
94428
|
-
}
|
|
94531
|
+
async function loadOrmSchema(paths, options) {
|
|
94429
94532
|
const ormFormat = parseOrmFormat(options.ormFormat);
|
|
94430
94533
|
const includesGlob = parseAgainstOrmValues(paths).some(hasGlobMagic);
|
|
94431
94534
|
const expandedPaths = await expandOrmPaths(paths);
|
|
@@ -94456,25 +94559,41 @@ async function runDrift(paths, options, config) {
|
|
|
94456
94559
|
if (includesGlob && inputs.some((input) => !isDdlFormat(input.format))) {
|
|
94457
94560
|
throw new Error("Glob ORM schema inputs are supported only for DDL");
|
|
94458
94561
|
}
|
|
94459
|
-
const merged = inputs.every((input) => isDdlFormat(input.format)) ? parseDdlFiles(inputs.map((input) => input.content), system) : mergeNormalizedSchemas(inputs.map(({ content, format }) => {
|
|
94562
|
+
const merged = inputs.every((input) => isDdlFormat(input.format)) ? parseDdlFiles(inputs.map((input) => input.content), options.system) : mergeNormalizedSchemas(inputs.map(({ content, format }) => {
|
|
94460
94563
|
if (format === "prisma")
|
|
94461
94564
|
return parsePrismaSchema(content);
|
|
94462
|
-
if (format === "ddl" || format in ORM_ALIASES)
|
|
94463
|
-
return parseDdl(content, system);
|
|
94464
|
-
}
|
|
94565
|
+
if (format === "ddl" || format in ORM_ALIASES)
|
|
94566
|
+
return parseDdl(content, options.system);
|
|
94465
94567
|
if (format === "drizzle")
|
|
94466
94568
|
return parseDrizzleSnapshot(JSON.parse(content));
|
|
94467
94569
|
const parsed = normalizedSchemaZod.parse(JSON.parse(content));
|
|
94468
94570
|
return { ...parsed, source: "json" };
|
|
94469
94571
|
}));
|
|
94470
94572
|
const alias = ormFormat && ormFormat in ORM_ALIASES ? ormFormat : undefined;
|
|
94471
|
-
|
|
94573
|
+
return {
|
|
94574
|
+
schema: alias ? { ...merged, source: alias } : merged,
|
|
94575
|
+
...alias ? { extraDefaultIgnore: [...ORM_ALIASES[alias].defaultIgnore] } : {}
|
|
94576
|
+
};
|
|
94577
|
+
}
|
|
94578
|
+
async function runDrift(paths, options, config) {
|
|
94579
|
+
const system = config.connection?.system;
|
|
94580
|
+
if (!system || !["postgresql", "mysql", "mariadb"].includes(system)) {
|
|
94581
|
+
throw new Error(`This command requires a SQL connection, got: ${system ?? "none"}`);
|
|
94582
|
+
}
|
|
94583
|
+
const cached2 = config.schema ?? {};
|
|
94584
|
+
if (Object.keys(cached2).length === 0) {
|
|
94585
|
+
throw new Error("Schema cache is empty. Run 'dbcli schema' first.");
|
|
94586
|
+
}
|
|
94587
|
+
const { schema: orm, extraDefaultIgnore } = await loadOrmSchema(paths, {
|
|
94588
|
+
...options.ormFormat !== undefined && { ormFormat: options.ormFormat },
|
|
94589
|
+
system
|
|
94590
|
+
});
|
|
94472
94591
|
const ignore = (options.ignore ?? "").split(",").map((pattern) => pattern.trim()).filter(Boolean);
|
|
94473
94592
|
const db = normalizeDbSchema(cached2, system === "postgresql" ? { defaultSchema: "public" } : undefined);
|
|
94474
94593
|
return {
|
|
94475
94594
|
report: compareNormalized(orm, db, {
|
|
94476
94595
|
ignore,
|
|
94477
|
-
extraDefaultIgnore
|
|
94596
|
+
extraDefaultIgnore
|
|
94478
94597
|
})
|
|
94479
94598
|
};
|
|
94480
94599
|
}
|
|
@@ -108079,6 +108198,632 @@ var init_semantic2 = __esm(() => {
|
|
|
108079
108198
|
});
|
|
108080
108199
|
});
|
|
108081
108200
|
|
|
108201
|
+
// src/core/design/index.ts
|
|
108202
|
+
import { join as join40 } from "path";
|
|
108203
|
+
function defaultDesignFile(workspaceRoot) {
|
|
108204
|
+
return join40(workspaceRoot, DEFAULT_FILE2);
|
|
108205
|
+
}
|
|
108206
|
+
function parseDesignSpec(raw, filePath = DEFAULT_FILE2) {
|
|
108207
|
+
const parsed = specSchema.safeParse(raw);
|
|
108208
|
+
if (!parsed.success) {
|
|
108209
|
+
throw new DesignValidationError(filePath, parsed.error.issues.map((issue2) => ({
|
|
108210
|
+
path: formatZodPath(issue2.path),
|
|
108211
|
+
message: issue2.message
|
|
108212
|
+
})));
|
|
108213
|
+
}
|
|
108214
|
+
return normalizeSpec(parsed.data);
|
|
108215
|
+
}
|
|
108216
|
+
async function loadDesignSpec(filePath) {
|
|
108217
|
+
const file = Bun.file(filePath);
|
|
108218
|
+
if (!await file.exists()) {
|
|
108219
|
+
throw new DesignValidationError(filePath, [{ path: "$", message: "file not found" }]);
|
|
108220
|
+
}
|
|
108221
|
+
if (file.size > MAX_FILE_BYTES2) {
|
|
108222
|
+
throw new DesignValidationError(filePath, [
|
|
108223
|
+
{ path: "$", message: `must not exceed ${MAX_FILE_BYTES2} bytes` }
|
|
108224
|
+
]);
|
|
108225
|
+
}
|
|
108226
|
+
try {
|
|
108227
|
+
return parseDesignSpec(JSON.parse(await file.text()), filePath);
|
|
108228
|
+
} catch (error) {
|
|
108229
|
+
if (error instanceof DesignValidationError)
|
|
108230
|
+
throw error;
|
|
108231
|
+
throw new DesignValidationError(filePath, [{ path: "$", message: "must be valid JSON" }]);
|
|
108232
|
+
}
|
|
108233
|
+
}
|
|
108234
|
+
function reviewDesign(spec) {
|
|
108235
|
+
const findings = [];
|
|
108236
|
+
const models = new Map;
|
|
108237
|
+
const tables = new Map;
|
|
108238
|
+
if (spec.models.length === 0)
|
|
108239
|
+
addFinding(findings, "error", "NO_MODELS", "$.models", "must contain at least one model");
|
|
108240
|
+
for (const [modelIndex, model] of spec.models.entries()) {
|
|
108241
|
+
const path6 = `$.models[${modelIndex}]`;
|
|
108242
|
+
if (models.has(model.name))
|
|
108243
|
+
addFinding(findings, "error", "DUPLICATE_MODEL", `${path6}.name`, "must be unique");
|
|
108244
|
+
else
|
|
108245
|
+
models.set(model.name, { model, index: modelIndex });
|
|
108246
|
+
if (tables.has(model.table))
|
|
108247
|
+
addFinding(findings, "error", "DUPLICATE_TABLE", `${path6}.table`, "must be unique");
|
|
108248
|
+
else
|
|
108249
|
+
tables.set(model.table, modelIndex);
|
|
108250
|
+
reviewModel(model, modelIndex, findings);
|
|
108251
|
+
}
|
|
108252
|
+
const relationships = new Set;
|
|
108253
|
+
for (const [relationshipIndex, relationship] of spec.relationships.entries()) {
|
|
108254
|
+
const path6 = `$.relationships[${relationshipIndex}]`;
|
|
108255
|
+
const key = `${relationship.from.model}\x00${relationship.from.field}\x00${relationship.to.model}\x00${relationship.to.field}`;
|
|
108256
|
+
const reverseKey = `${relationship.to.model}\x00${relationship.to.field}\x00${relationship.from.model}\x00${relationship.from.field}`;
|
|
108257
|
+
if (relationships.has(key))
|
|
108258
|
+
addFinding(findings, "error", "DUPLICATE_RELATIONSHIP", path6, "must not repeat relationship endpoints");
|
|
108259
|
+
else if (key !== reverseKey && relationships.has(reverseKey)) {
|
|
108260
|
+
addFinding(findings, "error", "REVERSE_RELATIONSHIP", path6, "must use one relationship direction; the reverse endpoints are already declared");
|
|
108261
|
+
}
|
|
108262
|
+
relationships.add(key);
|
|
108263
|
+
reviewRelationship(relationship, path6, models, findings);
|
|
108264
|
+
}
|
|
108265
|
+
for (const [patternIndex, pattern] of spec.accessPatterns.entries()) {
|
|
108266
|
+
reviewAccessPattern(pattern, `$.accessPatterns[${patternIndex}]`, models, findings);
|
|
108267
|
+
}
|
|
108268
|
+
findings.sort(findingOrder);
|
|
108269
|
+
return {
|
|
108270
|
+
findings,
|
|
108271
|
+
summary: {
|
|
108272
|
+
errors: findings.filter((finding) => finding.severity === "error").length,
|
|
108273
|
+
warns: findings.filter((finding) => finding.severity === "warn").length,
|
|
108274
|
+
infos: findings.filter((finding) => finding.severity === "info").length
|
|
108275
|
+
}
|
|
108276
|
+
};
|
|
108277
|
+
}
|
|
108278
|
+
function compileDesignSchema(spec) {
|
|
108279
|
+
const models = new Map(spec.models.map((model) => [model.name, model]));
|
|
108280
|
+
return {
|
|
108281
|
+
source: "design",
|
|
108282
|
+
...spec.dialect === "postgresql" ? { defaultSchema: "public" } : {},
|
|
108283
|
+
tables: spec.models.map((model) => ({
|
|
108284
|
+
identity: { table: model.table },
|
|
108285
|
+
columns: model.fields.map((field) => ({
|
|
108286
|
+
name: field.name,
|
|
108287
|
+
type: field.type,
|
|
108288
|
+
nullable: field.nullable,
|
|
108289
|
+
primaryKey: field.primaryKey || undefined
|
|
108290
|
+
})),
|
|
108291
|
+
indexes: [
|
|
108292
|
+
...model.indexes.map((index) => ({ ...index })),
|
|
108293
|
+
...model.fields.filter((field) => field.unique && !field.primaryKey).map((field) => ({ columns: [field.name], unique: true }))
|
|
108294
|
+
],
|
|
108295
|
+
foreignKeys: spec.relationships.flatMap((relationship) => {
|
|
108296
|
+
if (relationship.cardinality === "many-to-many" || relationship.from.model !== model.name)
|
|
108297
|
+
return [];
|
|
108298
|
+
const target = models.get(relationship.to.model);
|
|
108299
|
+
if (!target)
|
|
108300
|
+
return [];
|
|
108301
|
+
return [
|
|
108302
|
+
{
|
|
108303
|
+
columns: [relationship.from.field],
|
|
108304
|
+
refTable: { table: target.table },
|
|
108305
|
+
refColumns: [relationship.to.field]
|
|
108306
|
+
}
|
|
108307
|
+
];
|
|
108308
|
+
})
|
|
108309
|
+
})),
|
|
108310
|
+
unparsed: []
|
|
108311
|
+
};
|
|
108312
|
+
}
|
|
108313
|
+
function planDesignProposals(report) {
|
|
108314
|
+
return {
|
|
108315
|
+
report,
|
|
108316
|
+
proposals: report.entries.filter((entry) => entry.category !== "unmanaged").map((entry) => ({
|
|
108317
|
+
table: entry.table,
|
|
108318
|
+
object: entry.object,
|
|
108319
|
+
safety: entry.proposedCommands.some((command) => command.startsWith("# dry-run")) ? "dry-run" : "migration-review",
|
|
108320
|
+
commands: entry.proposedCommands,
|
|
108321
|
+
preflight: [
|
|
108322
|
+
"dbcli blacklist list",
|
|
108323
|
+
"Confirm the exact affected table with: dbcli schema <exact-table> --format json"
|
|
108324
|
+
],
|
|
108325
|
+
rollback: "Capture the current schema and generated DDL before any approved write; define the inverse migration before execution.",
|
|
108326
|
+
verification: [
|
|
108327
|
+
"After an approved write, run: dbcli schema <exact-table> --format json",
|
|
108328
|
+
"Re-run this same design diff command and review the remaining drift."
|
|
108329
|
+
]
|
|
108330
|
+
}))
|
|
108331
|
+
};
|
|
108332
|
+
}
|
|
108333
|
+
function reviewModel(model, modelIndex, findings) {
|
|
108334
|
+
const fields = new Map;
|
|
108335
|
+
const primaryKeys = model.fields.filter((field) => field.primaryKey);
|
|
108336
|
+
if (primaryKeys.length !== 1)
|
|
108337
|
+
addFinding(findings, "error", "PRIMARY_KEY_COUNT", `$.models[${modelIndex}].fields`, "must declare exactly one primary-key field in v1");
|
|
108338
|
+
for (const [fieldIndex, field] of model.fields.entries()) {
|
|
108339
|
+
const path6 = `$.models[${modelIndex}].fields[${fieldIndex}]`;
|
|
108340
|
+
if (fields.has(field.name))
|
|
108341
|
+
addFinding(findings, "error", "DUPLICATE_FIELD", `${path6}.name`, "must be unique within its model");
|
|
108342
|
+
else
|
|
108343
|
+
fields.set(field.name, fieldIndex);
|
|
108344
|
+
if (field.primaryKey && field.nullable)
|
|
108345
|
+
addFinding(findings, "error", "NULLABLE_PRIMARY_KEY", `${path6}.nullable`, "primary-key fields must not be nullable");
|
|
108346
|
+
}
|
|
108347
|
+
const indexKeys = new Set;
|
|
108348
|
+
for (const [indexIndex, index] of model.indexes.entries()) {
|
|
108349
|
+
const path6 = `$.models[${modelIndex}].indexes[${indexIndex}]`;
|
|
108350
|
+
const key = `${index.unique}\x00${index.columns.join("\x00")}`;
|
|
108351
|
+
if (indexKeys.has(key))
|
|
108352
|
+
addFinding(findings, "warn", "DUPLICATE_INDEX", path6, "duplicates a prior index");
|
|
108353
|
+
indexKeys.add(key);
|
|
108354
|
+
for (const column of index.columns) {
|
|
108355
|
+
if (!fields.has(column))
|
|
108356
|
+
addFinding(findings, "error", "UNKNOWN_INDEX_FIELD", `${path6}.columns`, `references unknown field '${column}'`);
|
|
108357
|
+
}
|
|
108358
|
+
if (index.columns.length === 1 && primaryKeys.some((field) => field.name === index.columns[0])) {
|
|
108359
|
+
addFinding(findings, "warn", "REDUNDANT_PRIMARY_KEY_INDEX", path6, "primary-key fields are already indexed");
|
|
108360
|
+
}
|
|
108361
|
+
if (!index.unique && model.indexes.some((candidate) => candidate.columns.length > index.columns.length && index.columns.every((column, columnIndex) => candidate.columns[columnIndex] === column))) {
|
|
108362
|
+
addFinding(findings, "warn", "PREFIX_REDUNDANT_INDEX", path6, "is covered by a longer index with the same leading columns");
|
|
108363
|
+
}
|
|
108364
|
+
}
|
|
108365
|
+
}
|
|
108366
|
+
function reviewRelationship(relationship, path6, models, findings) {
|
|
108367
|
+
const from = lookupField(relationship.from, `${path6}.from`, models, findings);
|
|
108368
|
+
const to = lookupField(relationship.to, `${path6}.to`, models, findings);
|
|
108369
|
+
if (!from || !to)
|
|
108370
|
+
return;
|
|
108371
|
+
if (relationship.cardinality === "many-to-many") {
|
|
108372
|
+
addFinding(findings, "error", "MANY_TO_MANY_REQUIRES_BRIDGE", `${path6}.cardinality`, "requires an explicit bridge model in v1");
|
|
108373
|
+
}
|
|
108374
|
+
if (typeFamily(from.field.type) !== typeFamily(to.field.type)) {
|
|
108375
|
+
addFinding(findings, "error", "RELATIONSHIP_TYPE_MISMATCH", path6, `field types are incompatible (${from.field.type} vs ${to.field.type})`);
|
|
108376
|
+
}
|
|
108377
|
+
if (relationship.cardinality === "one-to-one" && !isUniqueField(from.model, from.field.name)) {
|
|
108378
|
+
addFinding(findings, "error", "ONE_TO_ONE_REQUIRES_UNIQUE_FK", `${path6}.from.field`, "must be primary-key, unique, or covered by a single-column unique index");
|
|
108379
|
+
}
|
|
108380
|
+
}
|
|
108381
|
+
function reviewAccessPattern(pattern, path6, models, findings) {
|
|
108382
|
+
const found = models.get(pattern.model);
|
|
108383
|
+
if (!found) {
|
|
108384
|
+
addFinding(findings, "error", "UNKNOWN_ACCESS_MODEL", `${path6}.model`, `references unknown model '${pattern.model}'`);
|
|
108385
|
+
return;
|
|
108386
|
+
}
|
|
108387
|
+
const fields = new Set(found.model.fields.map((field) => field.name));
|
|
108388
|
+
for (const field of [...pattern.filters, ...pattern.sort]) {
|
|
108389
|
+
if (!fields.has(field))
|
|
108390
|
+
addFinding(findings, "error", "UNKNOWN_ACCESS_FIELD", path6, `references unknown field '${field}'`);
|
|
108391
|
+
}
|
|
108392
|
+
const needed = [...pattern.filters, ...pattern.sort];
|
|
108393
|
+
if (needed.length > 0 && !hasSupportingIndex(found.model, needed)) {
|
|
108394
|
+
addFinding(findings, "warn", "ACCESS_PATTERN_INDEX", path6, `consider an index beginning with (${needed.join(", ")})`);
|
|
108395
|
+
}
|
|
108396
|
+
}
|
|
108397
|
+
function lookupField(endpoint, path6, models, findings) {
|
|
108398
|
+
const found = models.get(endpoint.model);
|
|
108399
|
+
if (!found) {
|
|
108400
|
+
addFinding(findings, "error", "UNKNOWN_RELATIONSHIP_MODEL", `${path6}.model`, `references unknown model '${endpoint.model}'`);
|
|
108401
|
+
return;
|
|
108402
|
+
}
|
|
108403
|
+
const field = found.model.fields.find((candidate) => candidate.name === endpoint.field);
|
|
108404
|
+
if (!field) {
|
|
108405
|
+
addFinding(findings, "error", "UNKNOWN_RELATIONSHIP_FIELD", `${path6}.field`, `references unknown field '${endpoint.field}'`);
|
|
108406
|
+
return;
|
|
108407
|
+
}
|
|
108408
|
+
return { model: found.model, field };
|
|
108409
|
+
}
|
|
108410
|
+
function isUniqueField(model, field) {
|
|
108411
|
+
return model.fields.some((candidate) => candidate.name === field && (candidate.primaryKey || candidate.unique)) || model.indexes.some((index) => index.unique && index.columns.length === 1 && index.columns[0] === field);
|
|
108412
|
+
}
|
|
108413
|
+
function hasSupportingIndex(model, needed) {
|
|
108414
|
+
const indexes = [
|
|
108415
|
+
...model.indexes.map((index) => index.columns),
|
|
108416
|
+
...model.fields.filter((field) => field.primaryKey || field.unique).map((field) => [field.name])
|
|
108417
|
+
];
|
|
108418
|
+
return indexes.some((columns) => needed.every((column, index) => columns[index] === column));
|
|
108419
|
+
}
|
|
108420
|
+
function addFinding(findings, severity, code, path6, message) {
|
|
108421
|
+
findings.push({ severity, code, path: path6, message });
|
|
108422
|
+
}
|
|
108423
|
+
function normalizeSpec(spec) {
|
|
108424
|
+
return {
|
|
108425
|
+
...spec,
|
|
108426
|
+
models: [...spec.models].sort((left, right) => codePointOrder2(left.name, right.name)),
|
|
108427
|
+
relationships: [...spec.relationships].sort((left, right) => codePointOrder2(left.name, right.name)),
|
|
108428
|
+
accessPatterns: [...spec.accessPatterns].sort((left, right) => {
|
|
108429
|
+
const leftKey = `${left.model}\x00${left.filters.join("\x00")}\x00${left.sort.join("\x00")}`;
|
|
108430
|
+
const rightKey = `${right.model}\x00${right.filters.join("\x00")}\x00${right.sort.join("\x00")}`;
|
|
108431
|
+
return codePointOrder2(leftKey, rightKey);
|
|
108432
|
+
}),
|
|
108433
|
+
decisions: [...spec.decisions].sort((left, right) => codePointOrder2(left.name, right.name))
|
|
108434
|
+
};
|
|
108435
|
+
}
|
|
108436
|
+
function formatZodPath(path6) {
|
|
108437
|
+
return path6.reduce((result, part) => typeof part === "number" ? `${result}[${part}]` : `${result}.${part}`, "$");
|
|
108438
|
+
}
|
|
108439
|
+
function findingOrder(left, right) {
|
|
108440
|
+
return codePointOrder2(left.path, right.path) || codePointOrder2(left.code, right.code) || codePointOrder2(left.message, right.message);
|
|
108441
|
+
}
|
|
108442
|
+
function codePointOrder2(left, right) {
|
|
108443
|
+
const leftPoints = [...left];
|
|
108444
|
+
const rightPoints = [...right];
|
|
108445
|
+
for (let index = 0;index < Math.min(leftPoints.length, rightPoints.length); index += 1) {
|
|
108446
|
+
const difference = leftPoints[index].codePointAt(0) - rightPoints[index].codePointAt(0);
|
|
108447
|
+
if (difference !== 0)
|
|
108448
|
+
return difference;
|
|
108449
|
+
}
|
|
108450
|
+
return leftPoints.length - rightPoints.length;
|
|
108451
|
+
}
|
|
108452
|
+
var DesignValidationError, DEFAULT_FILE2 = "dbcli.design.json", MAX_FILE_BYTES2, MAX_MODELS2 = 100, MAX_FIELDS_PER_MODEL2 = 100, MAX_RELATIONSHIPS2 = 200, MAX_ACCESS_PATTERNS = 200, IDENTIFIER3, DESIGN_NAME, TYPE, UNSAFE_TEXT, identifier, designName, text2, fieldSchema, indexSchema, modelSchema, endpointSchema, relationshipSchema, accessPatternSchema, decisionSchema, specSchema;
|
|
108453
|
+
var init_design = __esm(() => {
|
|
108454
|
+
init_zod();
|
|
108455
|
+
init_normalized_schema();
|
|
108456
|
+
DesignValidationError = class DesignValidationError extends Error {
|
|
108457
|
+
filePath;
|
|
108458
|
+
issues;
|
|
108459
|
+
constructor(filePath, issues) {
|
|
108460
|
+
super(`Invalid design artifact at ${filePath}: ${issues.map((issue2) => `${issue2.path}: ${issue2.message}`).join("; ")}`);
|
|
108461
|
+
this.filePath = filePath;
|
|
108462
|
+
this.issues = issues;
|
|
108463
|
+
this.name = "DesignValidationError";
|
|
108464
|
+
}
|
|
108465
|
+
};
|
|
108466
|
+
MAX_FILE_BYTES2 = 256 * 1024;
|
|
108467
|
+
IDENTIFIER3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
108468
|
+
DESIGN_NAME = /^[a-z][a-z0-9-]*$/;
|
|
108469
|
+
TYPE = /^[^;\r\n]{1,100}$/;
|
|
108470
|
+
UNSAFE_TEXT = /(?:\b(?:select|insert|update|delete|alter|drop|grant)\b\s+|\bcreate\s+(?:table|index)\b|(?:postgres(?:ql)?|mysql):\/\/)/i;
|
|
108471
|
+
identifier = exports_external.string().regex(IDENTIFIER3, "must be a SQL-safe identifier");
|
|
108472
|
+
designName = exports_external.string().regex(DESIGN_NAME, "must be lowercase kebab-case");
|
|
108473
|
+
text2 = exports_external.string().min(1).max(1000).refine((value) => !UNSAFE_TEXT.test(value), "must not contain SQL or connection data");
|
|
108474
|
+
fieldSchema = exports_external.object({
|
|
108475
|
+
name: identifier,
|
|
108476
|
+
type: exports_external.string().regex(TYPE, "must be a bounded type declaration without SQL separators").refine((value) => !UNSAFE_TEXT.test(value), "must not contain SQL or connection data"),
|
|
108477
|
+
nullable: exports_external.boolean(),
|
|
108478
|
+
primaryKey: exports_external.boolean().optional().default(false),
|
|
108479
|
+
unique: exports_external.boolean().optional().default(false),
|
|
108480
|
+
description: text2.optional()
|
|
108481
|
+
}).strict();
|
|
108482
|
+
indexSchema = exports_external.object({
|
|
108483
|
+
name: identifier.optional(),
|
|
108484
|
+
columns: exports_external.array(identifier).min(1).max(16),
|
|
108485
|
+
unique: exports_external.boolean().optional().default(false)
|
|
108486
|
+
}).strict();
|
|
108487
|
+
modelSchema = exports_external.object({
|
|
108488
|
+
name: designName,
|
|
108489
|
+
table: identifier,
|
|
108490
|
+
description: text2.optional(),
|
|
108491
|
+
fields: exports_external.array(fieldSchema).max(MAX_FIELDS_PER_MODEL2),
|
|
108492
|
+
indexes: exports_external.array(indexSchema).optional().default([])
|
|
108493
|
+
}).strict();
|
|
108494
|
+
endpointSchema = exports_external.object({ model: designName, field: identifier }).strict();
|
|
108495
|
+
relationshipSchema = exports_external.object({
|
|
108496
|
+
name: designName,
|
|
108497
|
+
from: endpointSchema,
|
|
108498
|
+
to: endpointSchema,
|
|
108499
|
+
cardinality: exports_external.enum(["one-to-one", "one-to-many", "many-to-one", "many-to-many"]),
|
|
108500
|
+
description: text2.optional()
|
|
108501
|
+
}).strict();
|
|
108502
|
+
accessPatternSchema = exports_external.object({
|
|
108503
|
+
model: designName,
|
|
108504
|
+
filters: exports_external.array(identifier).max(16).optional().default([]),
|
|
108505
|
+
sort: exports_external.array(identifier).max(16).optional().default([]),
|
|
108506
|
+
description: text2.optional()
|
|
108507
|
+
}).strict();
|
|
108508
|
+
decisionSchema = exports_external.object({ name: designName, rationale: text2 }).strict();
|
|
108509
|
+
specSchema = exports_external.object({
|
|
108510
|
+
version: exports_external.literal(1),
|
|
108511
|
+
dialect: exports_external.enum(["postgresql", "mysql", "mariadb"]),
|
|
108512
|
+
models: exports_external.array(modelSchema).max(MAX_MODELS2),
|
|
108513
|
+
relationships: exports_external.array(relationshipSchema).max(MAX_RELATIONSHIPS2).optional().default([]),
|
|
108514
|
+
accessPatterns: exports_external.array(accessPatternSchema).max(MAX_ACCESS_PATTERNS).optional().default([]),
|
|
108515
|
+
decisions: exports_external.array(decisionSchema).max(100).optional().default([])
|
|
108516
|
+
}).strict();
|
|
108517
|
+
});
|
|
108518
|
+
|
|
108519
|
+
// src/formatters/design.ts
|
|
108520
|
+
function formatDesign(spec, review, format) {
|
|
108521
|
+
if (format === "json")
|
|
108522
|
+
return JSON.stringify({ spec, review }, null, 2);
|
|
108523
|
+
if (format === "mermaid")
|
|
108524
|
+
return formatMermaid(spec);
|
|
108525
|
+
return formatMarkdown2(spec, review);
|
|
108526
|
+
}
|
|
108527
|
+
function formatDesignReview(review, format) {
|
|
108528
|
+
if (format === "json")
|
|
108529
|
+
return JSON.stringify(review, null, 2);
|
|
108530
|
+
const lines = ["# Design review", ""];
|
|
108531
|
+
if (review.findings.length === 0)
|
|
108532
|
+
lines.push("No design findings.");
|
|
108533
|
+
else {
|
|
108534
|
+
lines.push("| Severity | Code | Location | Finding |", "| --- | --- | --- | --- |");
|
|
108535
|
+
for (const finding of review.findings) {
|
|
108536
|
+
lines.push(`| ${finding.severity} | ${finding.code} | ${escapeTable(finding.path)} | ${escapeTable(finding.message)} |`);
|
|
108537
|
+
}
|
|
108538
|
+
}
|
|
108539
|
+
lines.push("", `Summary: ${review.summary.errors} error(s), ${review.summary.warns} warning(s), ${review.summary.infos} info(s).`);
|
|
108540
|
+
return lines.join(`
|
|
108541
|
+
`);
|
|
108542
|
+
}
|
|
108543
|
+
function formatDesignProposal(plan, format) {
|
|
108544
|
+
if (format === "json")
|
|
108545
|
+
return JSON.stringify(plan, null, 2);
|
|
108546
|
+
const lines = ["# Design change proposal", ""];
|
|
108547
|
+
if (plan.proposals.length === 0)
|
|
108548
|
+
lines.push("No design changes need a proposal.");
|
|
108549
|
+
for (const proposal of plan.proposals) {
|
|
108550
|
+
lines.push(`## ${proposal.table}.${proposal.object}`, "", `Safety: **${proposal.safety}**`, "", "Preflight:");
|
|
108551
|
+
for (const step of proposal.preflight)
|
|
108552
|
+
lines.push(`- ${inlineCode2(step)}`);
|
|
108553
|
+
lines.push("", "Proposed command or escalation:");
|
|
108554
|
+
lines.push("```bash", ...proposal.commands, "```", "", `Rollback: ${proposal.rollback}`, "", "Verification:");
|
|
108555
|
+
for (const step of proposal.verification)
|
|
108556
|
+
lines.push(`- ${inlineCode2(step)}`);
|
|
108557
|
+
lines.push("");
|
|
108558
|
+
}
|
|
108559
|
+
lines.push(`Drift summary: ${plan.report.summary.errors} error(s), ${plan.report.summary.warns} warning(s).`);
|
|
108560
|
+
return lines.join(`
|
|
108561
|
+
`);
|
|
108562
|
+
}
|
|
108563
|
+
function formatMarkdown2(spec, review) {
|
|
108564
|
+
const lines = ["# Database Design", "", `Dialect: \`${spec.dialect}\``, ""];
|
|
108565
|
+
for (const model of spec.models) {
|
|
108566
|
+
lines.push(`## ${model.name}`, "", `Physical table: \`${model.table}\``);
|
|
108567
|
+
if (model.description)
|
|
108568
|
+
lines.push("", escapeText(model.description));
|
|
108569
|
+
lines.push("", "| Field | Type | Nullable | Key |", "| --- | --- | --- | --- |");
|
|
108570
|
+
for (const field of model.fields) {
|
|
108571
|
+
const key = field.primaryKey ? "primary" : field.unique ? "unique" : "";
|
|
108572
|
+
lines.push(`| \`${field.name}\` | \`${field.type}\` | ${field.nullable ? "yes" : "no"} | ${key} |`);
|
|
108573
|
+
}
|
|
108574
|
+
if (model.indexes.length > 0) {
|
|
108575
|
+
lines.push("", "Indexes:");
|
|
108576
|
+
for (const index of model.indexes) {
|
|
108577
|
+
lines.push(`- ${index.unique ? "unique " : ""}index on \`${index.columns.join(", ")}\``);
|
|
108578
|
+
}
|
|
108579
|
+
}
|
|
108580
|
+
lines.push("");
|
|
108581
|
+
}
|
|
108582
|
+
if (spec.relationships.length > 0) {
|
|
108583
|
+
lines.push("## Relationships", "");
|
|
108584
|
+
for (const relationship of spec.relationships) {
|
|
108585
|
+
lines.push(`- \`${relationship.name}\`: \`${relationship.from.model}.${relationship.from.field}\` \u2192 \`${relationship.to.model}.${relationship.to.field}\` (${relationship.cardinality})`);
|
|
108586
|
+
}
|
|
108587
|
+
lines.push("");
|
|
108588
|
+
}
|
|
108589
|
+
if (spec.accessPatterns.length > 0) {
|
|
108590
|
+
lines.push("## Access patterns", "");
|
|
108591
|
+
for (const pattern of spec.accessPatterns) {
|
|
108592
|
+
lines.push(`- \`${pattern.model}\`: filter ${inlineList(pattern.filters)}; sort ${inlineList(pattern.sort)}`);
|
|
108593
|
+
}
|
|
108594
|
+
lines.push("");
|
|
108595
|
+
}
|
|
108596
|
+
if (spec.decisions.length > 0) {
|
|
108597
|
+
lines.push("## Decisions", "");
|
|
108598
|
+
for (const decision of spec.decisions)
|
|
108599
|
+
lines.push(`- **${decision.name}** \u2014 ${escapeText(decision.rationale)}`);
|
|
108600
|
+
lines.push("");
|
|
108601
|
+
}
|
|
108602
|
+
lines.push(formatDesignReview(review, "markdown"));
|
|
108603
|
+
return lines.join(`
|
|
108604
|
+
`);
|
|
108605
|
+
}
|
|
108606
|
+
function formatMermaid(spec) {
|
|
108607
|
+
const lines = ["erDiagram"];
|
|
108608
|
+
for (const model of spec.models) {
|
|
108609
|
+
lines.push(` ${model.table} {`);
|
|
108610
|
+
for (const field of model.fields) {
|
|
108611
|
+
const markers = [field.primaryKey ? "PK" : "", field.unique && !field.primaryKey ? "UK" : ""].filter(Boolean).join(", ");
|
|
108612
|
+
lines.push(` ${mermaidType(field.type)} ${field.name}${markers ? ` "${markers}"` : ""}`);
|
|
108613
|
+
}
|
|
108614
|
+
lines.push(" }");
|
|
108615
|
+
}
|
|
108616
|
+
const models = new Map(spec.models.map((model) => [model.name, model]));
|
|
108617
|
+
for (const relationship of spec.relationships) {
|
|
108618
|
+
const from = models.get(relationship.from.model);
|
|
108619
|
+
const to = models.get(relationship.to.model);
|
|
108620
|
+
if (!from || !to)
|
|
108621
|
+
continue;
|
|
108622
|
+
lines.push(` ${from.table} ${mermaidCardinality(relationship.cardinality)} ${to.table} : "${relationship.name}"`);
|
|
108623
|
+
}
|
|
108624
|
+
return lines.join(`
|
|
108625
|
+
`);
|
|
108626
|
+
}
|
|
108627
|
+
function mermaidCardinality(cardinality) {
|
|
108628
|
+
if (cardinality === "one-to-one")
|
|
108629
|
+
return "||--||";
|
|
108630
|
+
if (cardinality === "one-to-many")
|
|
108631
|
+
return "||--o{";
|
|
108632
|
+
if (cardinality === "many-to-one")
|
|
108633
|
+
return "}o--||";
|
|
108634
|
+
return "}o--o{";
|
|
108635
|
+
}
|
|
108636
|
+
function mermaidType(type) {
|
|
108637
|
+
return type.replace(/[^A-Za-z0-9_]/g, "_");
|
|
108638
|
+
}
|
|
108639
|
+
function inlineList(values) {
|
|
108640
|
+
return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
|
|
108641
|
+
}
|
|
108642
|
+
function inlineCode2(value) {
|
|
108643
|
+
return `\`${value.replace(/`/g, "\\`")}\``;
|
|
108644
|
+
}
|
|
108645
|
+
function escapeTable(value) {
|
|
108646
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r\n|\r|\n/g, "<br>");
|
|
108647
|
+
}
|
|
108648
|
+
function escapeText(value) {
|
|
108649
|
+
return value.replace(/([`*_{}[\]()#+.!|>~-])/g, "\\$1").replace(/\r\n|\r|\n/g, "<br>");
|
|
108650
|
+
}
|
|
108651
|
+
|
|
108652
|
+
// src/commands/design.ts
|
|
108653
|
+
function defaultFile(file) {
|
|
108654
|
+
return file ?? defaultDesignFile(process.cwd());
|
|
108655
|
+
}
|
|
108656
|
+
function collectOption2(value, previous) {
|
|
108657
|
+
return [...previous, value];
|
|
108658
|
+
}
|
|
108659
|
+
function parseIgnore(value) {
|
|
108660
|
+
return (value ?? "").split(",").map((pattern) => pattern.trim()).filter(Boolean);
|
|
108661
|
+
}
|
|
108662
|
+
function template(dialect) {
|
|
108663
|
+
return JSON.stringify({
|
|
108664
|
+
version: 1,
|
|
108665
|
+
dialect,
|
|
108666
|
+
models: [],
|
|
108667
|
+
relationships: [],
|
|
108668
|
+
accessPatterns: [],
|
|
108669
|
+
decisions: []
|
|
108670
|
+
}, null, 2) + `
|
|
108671
|
+
`;
|
|
108672
|
+
}
|
|
108673
|
+
function fail2(error, format) {
|
|
108674
|
+
if (error instanceof DesignValidationError) {
|
|
108675
|
+
const report = {
|
|
108676
|
+
findings: error.issues.map((issue2) => ({
|
|
108677
|
+
...issue2,
|
|
108678
|
+
code: "INVALID_ARTIFACT",
|
|
108679
|
+
severity: "error"
|
|
108680
|
+
})),
|
|
108681
|
+
summary: { errors: error.issues.length, warns: 0, infos: 0 }
|
|
108682
|
+
};
|
|
108683
|
+
console.log(formatDesignReview(report, format));
|
|
108684
|
+
} else {
|
|
108685
|
+
console.error(error.message);
|
|
108686
|
+
}
|
|
108687
|
+
process.exit(1);
|
|
108688
|
+
}
|
|
108689
|
+
async function compareAgainstCache(desired, dialect, ignore, command) {
|
|
108690
|
+
const config = await configModule.read(resolveConfigPath(command));
|
|
108691
|
+
const system = config.connection?.system;
|
|
108692
|
+
if (!system || !["postgresql", "mysql", "mariadb"].includes(system)) {
|
|
108693
|
+
throw new Error("design comparison against cache requires a configured PostgreSQL, MySQL, or MariaDB connection");
|
|
108694
|
+
}
|
|
108695
|
+
if (system !== dialect) {
|
|
108696
|
+
throw new Error(`design dialect '${dialect}' does not match configured connection '${system}'`);
|
|
108697
|
+
}
|
|
108698
|
+
if (Object.keys(config.schema ?? {}).length === 0) {
|
|
108699
|
+
throw new Error("Schema cache is empty. Run 'dbcli schema' first.");
|
|
108700
|
+
}
|
|
108701
|
+
const actual = normalizeDbSchema(config.schema, system === "postgresql" ? { defaultSchema: "public" } : {});
|
|
108702
|
+
return compareNormalized(desired, actual, { ignore });
|
|
108703
|
+
}
|
|
108704
|
+
async function compareAgainstOrm(desired, dialect, paths, ormFormat, ignore) {
|
|
108705
|
+
const { schema: orm, extraDefaultIgnore } = await loadOrmSchema(parseAgainstOrmValues(paths), {
|
|
108706
|
+
...ormFormat !== undefined && { ormFormat },
|
|
108707
|
+
system: dialect
|
|
108708
|
+
});
|
|
108709
|
+
return compareNormalized(desired, orm, {
|
|
108710
|
+
ignore,
|
|
108711
|
+
extraDefaultIgnore,
|
|
108712
|
+
targetLabel: "ORM definition"
|
|
108713
|
+
});
|
|
108714
|
+
}
|
|
108715
|
+
function isFormat(value, formats) {
|
|
108716
|
+
return value !== undefined && formats.includes(value);
|
|
108717
|
+
}
|
|
108718
|
+
var VALIDATE_FORMATS, RENDER_FORMATS, DIFF_FORMATS, designCommand;
|
|
108719
|
+
var init_design2 = __esm(() => {
|
|
108720
|
+
init_esm();
|
|
108721
|
+
init_design();
|
|
108722
|
+
init_config();
|
|
108723
|
+
init_from_db();
|
|
108724
|
+
init_compare();
|
|
108725
|
+
init_diff();
|
|
108726
|
+
init_config_path();
|
|
108727
|
+
VALIDATE_FORMATS = ["json", "markdown"];
|
|
108728
|
+
RENDER_FORMATS = ["json", "markdown", "mermaid"];
|
|
108729
|
+
DIFF_FORMATS = ["json", "table", "markdown"];
|
|
108730
|
+
designCommand = new Command("design").description("Validate, render, and safely review a version-controlled SQL database design");
|
|
108731
|
+
designCommand.command("init").description("Write an empty design artifact only to the explicitly supplied output path").requiredOption("--output <path>", "Path for the new design JSON artifact").option("--dialect <dialect>", "Target SQL dialect: postgresql, mysql, or mariadb", "postgresql").action(async (options) => {
|
|
108732
|
+
try {
|
|
108733
|
+
if (!["postgresql", "mysql", "mariadb"].includes(options.dialect)) {
|
|
108734
|
+
throw new Error("dialect must be postgresql, mysql, or mariadb");
|
|
108735
|
+
}
|
|
108736
|
+
const file = Bun.file(options.output);
|
|
108737
|
+
if (await file.exists())
|
|
108738
|
+
throw new Error(`refusing to overwrite existing file: ${options.output}`);
|
|
108739
|
+
await Bun.write(options.output, template(options.dialect));
|
|
108740
|
+
console.log(JSON.stringify({ status: "created", path: options.output }, null, 2));
|
|
108741
|
+
} catch (error) {
|
|
108742
|
+
console.error(error.message);
|
|
108743
|
+
process.exit(1);
|
|
108744
|
+
}
|
|
108745
|
+
});
|
|
108746
|
+
designCommand.command("diff").description("Compare a valid design against the local schema cache or local ORM definition without connecting").option("--file <path>", "Design JSON file (default: dbcli.design.json)").option("--against-cache", "Compare against the configured local schema cache").option("--against-orm <paths>", "Compare against ORM definition(s), repeatable or comma-separated; DDL supports globs", collectOption2, []).option("--orm-format <format>", "Force ORM input: prisma | ddl | json | drizzle | typeorm | sequelize").option("--ignore <globs>", "Comma-separated table globs excluded from drift").option("--format <format>", "Output format: json, table, or markdown", "json").action(async (options, command) => {
|
|
108747
|
+
const format = options.format;
|
|
108748
|
+
if (!isFormat(format, DIFF_FORMATS))
|
|
108749
|
+
throw new Error("format must be json, table, or markdown");
|
|
108750
|
+
try {
|
|
108751
|
+
const spec = await loadDesignSpec(defaultFile(options.file));
|
|
108752
|
+
const review = reviewDesign(spec);
|
|
108753
|
+
if (review.summary.errors > 0) {
|
|
108754
|
+
console.log(formatDesignReview(review, format === "table" ? "markdown" : format));
|
|
108755
|
+
process.exitCode = 1;
|
|
108756
|
+
return;
|
|
108757
|
+
}
|
|
108758
|
+
const modeCount = Number(Boolean(options.againstCache)) + Number((options.againstOrm?.length ?? 0) > 0);
|
|
108759
|
+
if (modeCount !== 1) {
|
|
108760
|
+
throw new Error("Choose exactly one of --against-cache or --against-orm");
|
|
108761
|
+
}
|
|
108762
|
+
const desired = compileDesignSchema(spec);
|
|
108763
|
+
const ignore = parseIgnore(options.ignore);
|
|
108764
|
+
const report = options.againstCache ? await compareAgainstCache(desired, spec.dialect, ignore, command) : await compareAgainstOrm(desired, spec.dialect, options.againstOrm ?? [], options.ormFormat, ignore);
|
|
108765
|
+
console.log(formatDrift(report, format));
|
|
108766
|
+
process.exitCode = report.summary.errors > 0 ? 1 : 0;
|
|
108767
|
+
} catch (error) {
|
|
108768
|
+
fail2(error, format === "table" ? "markdown" : format);
|
|
108769
|
+
}
|
|
108770
|
+
});
|
|
108771
|
+
designCommand.command("propose").description("Produce a review-only migration proposal from design drift; never executes DDL").option("--file <path>", "Design JSON file (default: dbcli.design.json)").option("--against-cache", "Compare against the configured local schema cache").option("--against-orm <paths>", "Compare against ORM definition(s), repeatable or comma-separated; DDL supports globs", collectOption2, []).option("--orm-format <format>", "Force ORM input: prisma | ddl | json | drizzle | typeorm | sequelize").option("--ignore <globs>", "Comma-separated table globs excluded from drift").option("--format <format>", "Output format: json or markdown", "markdown").action(async (options, command) => {
|
|
108772
|
+
const format = options.format;
|
|
108773
|
+
if (!isFormat(format, VALIDATE_FORMATS))
|
|
108774
|
+
throw new Error("format must be json or markdown");
|
|
108775
|
+
try {
|
|
108776
|
+
const spec = await loadDesignSpec(defaultFile(options.file));
|
|
108777
|
+
const review = reviewDesign(spec);
|
|
108778
|
+
if (review.summary.errors > 0) {
|
|
108779
|
+
console.log(formatDesignReview(review, format));
|
|
108780
|
+
process.exitCode = 1;
|
|
108781
|
+
return;
|
|
108782
|
+
}
|
|
108783
|
+
const modeCount = Number(Boolean(options.againstCache)) + Number((options.againstOrm?.length ?? 0) > 0);
|
|
108784
|
+
if (modeCount !== 1) {
|
|
108785
|
+
throw new Error("Choose exactly one of --against-cache or --against-orm");
|
|
108786
|
+
}
|
|
108787
|
+
const desired = compileDesignSchema(spec);
|
|
108788
|
+
const ignore = parseIgnore(options.ignore);
|
|
108789
|
+
const report = options.againstCache ? await compareAgainstCache(desired, spec.dialect, ignore, command) : await compareAgainstOrm(desired, spec.dialect, options.againstOrm ?? [], options.ormFormat, ignore);
|
|
108790
|
+
console.log(formatDesignProposal(planDesignProposals(report), format));
|
|
108791
|
+
process.exitCode = report.summary.errors > 0 ? 1 : 0;
|
|
108792
|
+
} catch (error) {
|
|
108793
|
+
fail2(error, format);
|
|
108794
|
+
}
|
|
108795
|
+
});
|
|
108796
|
+
designCommand.command("validate").description("Validate a local SQL design artifact without a database connection").option("--file <path>", "Design JSON file (default: dbcli.design.json)").option("--format <format>", "Output format: json or markdown", "json").action(async (options) => {
|
|
108797
|
+
const format = options.format;
|
|
108798
|
+
if (!isFormat(format, VALIDATE_FORMATS))
|
|
108799
|
+
throw new Error("format must be json or markdown");
|
|
108800
|
+
try {
|
|
108801
|
+
const review = reviewDesign(await loadDesignSpec(defaultFile(options.file)));
|
|
108802
|
+
console.log(formatDesignReview(review, format));
|
|
108803
|
+
process.exitCode = review.summary.errors > 0 ? 1 : 0;
|
|
108804
|
+
} catch (error) {
|
|
108805
|
+
fail2(error, format);
|
|
108806
|
+
}
|
|
108807
|
+
});
|
|
108808
|
+
designCommand.command("render").description("Render a valid local SQL design as JSON, Markdown, or Mermaid ERD").option("--file <path>", "Design JSON file (default: dbcli.design.json)").option("--format <format>", "Output format: json, markdown, or mermaid", "markdown").action(async (options) => {
|
|
108809
|
+
const format = options.format;
|
|
108810
|
+
if (!isFormat(format, RENDER_FORMATS))
|
|
108811
|
+
throw new Error("format must be json, markdown, or mermaid");
|
|
108812
|
+
try {
|
|
108813
|
+
const spec = await loadDesignSpec(defaultFile(options.file));
|
|
108814
|
+
const review = reviewDesign(spec);
|
|
108815
|
+
if (review.summary.errors > 0) {
|
|
108816
|
+
console.log(formatDesignReview(review, format === "mermaid" ? "markdown" : format));
|
|
108817
|
+
process.exitCode = 1;
|
|
108818
|
+
return;
|
|
108819
|
+
}
|
|
108820
|
+
console.log(formatDesign(spec, review, format));
|
|
108821
|
+
} catch (error) {
|
|
108822
|
+
fail2(error, format === "mermaid" ? "markdown" : format);
|
|
108823
|
+
}
|
|
108824
|
+
});
|
|
108825
|
+
});
|
|
108826
|
+
|
|
108082
108827
|
// src/core/backfill-artifact.ts
|
|
108083
108828
|
import { createHash as createHash5 } from "crypto";
|
|
108084
108829
|
function isRecord3(value) {
|
|
@@ -108360,6 +109105,13 @@ function parsePositiveInteger(value) {
|
|
|
108360
109105
|
}
|
|
108361
109106
|
return parsed;
|
|
108362
109107
|
}
|
|
109108
|
+
function parseSlowMs(value) {
|
|
109109
|
+
try {
|
|
109110
|
+
return parseSlowQueryThreshold(value);
|
|
109111
|
+
} catch {
|
|
109112
|
+
throw new InvalidArgumentError("must be a non-negative integer");
|
|
109113
|
+
}
|
|
109114
|
+
}
|
|
108363
109115
|
function normalizeLimitFlags(options) {
|
|
108364
109116
|
const limit = options.limit;
|
|
108365
109117
|
if (typeof limit === "boolean") {
|
|
@@ -108396,7 +109148,7 @@ function buildProgram() {
|
|
|
108396
109148
|
program2.addCommand(initCommand);
|
|
108397
109149
|
program2.addCommand(listCommand);
|
|
108398
109150
|
program2.addCommand(schemaCommand);
|
|
108399
|
-
program2.command("query [sql]").description(t("query.description")).option("-f, --query-file <path>", "Read query text from a UTF-8 file, or - for stdin").option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--limit <number>", "Limit result rows (overrides auto-limit)", parsePositiveInteger).option("--no-limit", "Disable auto-limit in query-only mode").option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--fields <list>", "Include fields, or exclude them with --fields=-field_a,-field_b").option("--truncate <number>", "Limit serialized table cells to N Unicode characters", parsePositiveInteger).option("--no-truncate", "Disable the default table-cell truncation").addOption(createConnectionSelectorOption()).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
|
|
109151
|
+
program2.command("query [sql]").description(t("query.description")).option("-f, --query-file <path>", "Read query text from a UTF-8 file, or - for stdin").option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).option("--limit <number>", "Limit result rows (overrides auto-limit)", parsePositiveInteger).option("--no-limit", "Disable auto-limit in query-only mode").option("--collection <name>", "MongoDB collection name; Elasticsearch index name").option("--index <name>", "Elasticsearch index name (alias for --collection)").option("--fields <list>", "Include fields, or exclude them with --fields=-field_a,-field_b").option("--truncate <number>", "Limit serialized table cells to N Unicode characters", parsePositiveInteger).option("--no-truncate", "Disable the default table-cell truncation").option("--slow-ms <number>", "Emit a passive performance hint at or above this execution time; 0 disables it", parseSlowMs, 1000).addOption(createConnectionSelectorOption()).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (sql, options, command) => {
|
|
108400
109152
|
const rawArgs = command.parent?.rawArgs ?? [];
|
|
108401
109153
|
const rawTruncate = findLongOptionValue(rawArgs, "--truncate");
|
|
108402
109154
|
const rootUse = command.parent?.opts().use;
|
|
@@ -108412,7 +109164,7 @@ function buildProgram() {
|
|
|
108412
109164
|
program2.command("plan <sql>").description("Analyze SQL risk without executing").option("--format <type>", "Output format: text, json", "text").action(async (sql, options, command) => {
|
|
108413
109165
|
await planCommand(sql, options, command);
|
|
108414
109166
|
});
|
|
108415
|
-
program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).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").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).option("--verify", "Run verification check after execution if defined", false).action(async (name2, options, command) => {
|
|
109167
|
+
program2.command("q <name>").description(t("q.description")).option("--format <type>", "Output format: table, json, csv, html", "table").option("--ui", "Show interactive dashboard in browser", false).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").option("--slow-ms <number>", "Emit a passive performance hint at or above this execution time; 0 disables it", parseSlowMs, 1000).option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).option("--verify", "Run verification check after execution if defined", false).action(async (name2, options, command) => {
|
|
108416
109168
|
await qCommand(name2, normalizeLimitFlags(options), command);
|
|
108417
109169
|
});
|
|
108418
109170
|
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").option("--plan", "Analyze risk without connecting or executing").option("--format <type>", "Output format for --plan: text or json", "text").option("--recovery", "On failure, emit a structured recovery envelope to stdout (suppresses human stderr message)", false).action(async (table, options, command) => {
|
|
@@ -108482,6 +109234,7 @@ function buildProgram() {
|
|
|
108482
109234
|
program2.addCommand(verifyCommand);
|
|
108483
109235
|
program2.addCommand(proxyCommand);
|
|
108484
109236
|
program2.addCommand(semanticCommand);
|
|
109237
|
+
program2.addCommand(designCommand);
|
|
108485
109238
|
program2.addCommand(backfillCommand);
|
|
108486
109239
|
return program2;
|
|
108487
109240
|
}
|
|
@@ -108526,10 +109279,12 @@ var init_program = __esm(() => {
|
|
|
108526
109279
|
init_use();
|
|
108527
109280
|
init_proxy();
|
|
108528
109281
|
init_semantic2();
|
|
109282
|
+
init_design2();
|
|
108529
109283
|
init_backfill();
|
|
108530
109284
|
init_connection_selector();
|
|
108531
109285
|
init_cli_error();
|
|
108532
109286
|
init_validation();
|
|
109287
|
+
init_slow_query_advisory();
|
|
108533
109288
|
init_package();
|
|
108534
109289
|
});
|
|
108535
109290
|
|
|
@@ -108655,7 +109410,7 @@ async function claimUpdateHint(configPath, kind, sessionKey, value) {
|
|
|
108655
109410
|
// src/cli.ts
|
|
108656
109411
|
init_program();
|
|
108657
109412
|
init_cli_error();
|
|
108658
|
-
import { join as
|
|
109413
|
+
import { join as join41 } from "path";
|
|
108659
109414
|
import { writeSync as writeSync2 } from "fs";
|
|
108660
109415
|
import { format } from "util";
|
|
108661
109416
|
function installSynchronousRedirectedStdout() {
|
|
@@ -108784,7 +109539,7 @@ program2.hook("preAction", (thisCommand, actionCommand) => {
|
|
|
108784
109539
|
try {
|
|
108785
109540
|
let cache = null;
|
|
108786
109541
|
try {
|
|
108787
|
-
const cacheFile = Bun.file(
|
|
109542
|
+
const cacheFile = Bun.file(join41(configPath, "version-check.json"));
|
|
108788
109543
|
if (await cacheFile.exists()) {
|
|
108789
109544
|
cache = await cacheFile.json();
|
|
108790
109545
|
}
|