@carllee1983/dbcli 1.37.1 → 1.38.1
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 +192 -281
- package/.cursor/skills/dbcli/reference.md +68 -0
- package/.github/skills/dbcli/SKILL.md +192 -281
- package/.github/skills/dbcli/reference.md +68 -0
- package/CHANGELOG.md +16 -0
- package/assets/SKILL.md +192 -281
- package/assets/SKILL.zh-TW.md +211 -331
- package/assets/reference.md +68 -0
- package/dist/cli.mjs +506 -24
- package/dist/core.mjs +1 -1
- package/package.json +1 -1
- package/plugins/dbcli-agent/skills/dbcli/SKILL.md +192 -281
- package/plugins/dbcli-agent/skills/dbcli/reference.md +68 -0
- package/skills/dbcli/SKILL.md +192 -281
- package/skills/dbcli/reference.md +68 -0
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.38.1",
|
|
56
56
|
description: "Database CLI for AI agents",
|
|
57
57
|
type: "module",
|
|
58
58
|
publishConfig: {
|
|
@@ -23087,7 +23087,7 @@ var init_capabilities = __esm(() => {
|
|
|
23087
23087
|
queries: cap("limited", "local-write", "Snippet management works with Redis-specific saved-query limitations."),
|
|
23088
23088
|
insert: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
|
|
23089
23089
|
update: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
|
|
23090
|
-
delete: cap("
|
|
23090
|
+
delete: cap("limited", "db-write", "Basic delete via DEL/HDEL/LREM/SREM/ZREM (needs data-admin); supports --dry-run."),
|
|
23091
23091
|
export: cap("unsupported", "none", "Redis export is not supported."),
|
|
23092
23092
|
blacklist: cap("limited", "local-write", "Key-glob rejection (Redis-native pattern) plus value/hash-field masking ([REDACTED])."),
|
|
23093
23093
|
check: cap("unsupported", "none", "Data health check is SQL-only."),
|
|
@@ -28927,6 +28927,7 @@ var init_assert_artifact = __esm(() => {
|
|
|
28927
28927
|
"assertion",
|
|
28928
28928
|
"migration",
|
|
28929
28929
|
"backfill",
|
|
28930
|
+
"table",
|
|
28930
28931
|
"manual"
|
|
28931
28932
|
];
|
|
28932
28933
|
AssertArtifactError = class AssertArtifactError extends Error {
|
|
@@ -92930,12 +92931,277 @@ var init_rollback = __esm(() => {
|
|
|
92930
92931
|
note: "Preflight guards ran before post-rollback read-back verification."
|
|
92931
92932
|
};
|
|
92932
92933
|
});
|
|
92934
|
+
|
|
92935
|
+
// src/core/verify/constraint.ts
|
|
92936
|
+
function normalizeConstraintCheck(raw) {
|
|
92937
|
+
const check2 = raw ?? "";
|
|
92938
|
+
if (!ALLOWED_CONSTRAINT_CHECKS.includes(check2)) {
|
|
92939
|
+
throw new VerifyInputError(`Invalid --check '${check2}'. Allowed: ${ALLOWED_CONSTRAINT_CHECKS.join(", ")}`);
|
|
92940
|
+
}
|
|
92941
|
+
return check2;
|
|
92942
|
+
}
|
|
92943
|
+
function toColumns(raw) {
|
|
92944
|
+
if (raw === undefined || raw === null)
|
|
92945
|
+
return [];
|
|
92946
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
92947
|
+
return arr.map((c2) => requireNonEmpty(c2, "--column"));
|
|
92948
|
+
}
|
|
92949
|
+
function parseReferences(raw) {
|
|
92950
|
+
const ref = requireNonEmpty(raw, "--references");
|
|
92951
|
+
const parts = ref.split(".").map((p) => p.trim());
|
|
92952
|
+
if (parts.length < 2 || parts.some((p) => p.length === 0)) {
|
|
92953
|
+
throw new VerifyInputError(`--references must be '<table>.<column>' (got '${ref}')`);
|
|
92954
|
+
}
|
|
92955
|
+
const column = parts.pop();
|
|
92956
|
+
const table = parts.join(".");
|
|
92957
|
+
return { table, column };
|
|
92958
|
+
}
|
|
92959
|
+
function parseBaseline(raw) {
|
|
92960
|
+
if (raw === undefined || raw === null || raw === "")
|
|
92961
|
+
return 0;
|
|
92962
|
+
const n = Number(raw);
|
|
92963
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
92964
|
+
throw new VerifyInputError(`--baseline must be a non-negative integer (got '${String(raw)}')`);
|
|
92965
|
+
}
|
|
92966
|
+
return n;
|
|
92967
|
+
}
|
|
92968
|
+
function normalizeConstraintInput(raw) {
|
|
92969
|
+
const check2 = normalizeConstraintCheck(raw.check);
|
|
92970
|
+
const table = requireNonEmpty(raw.table, "--table");
|
|
92971
|
+
const format = normalizeFormat(raw.format);
|
|
92972
|
+
const columns = toColumns(raw.column);
|
|
92973
|
+
const violationQueryRaw = raw.violationQuery;
|
|
92974
|
+
const hasViolationQuery = typeof violationQueryRaw === "string" && violationQueryRaw.trim().length > 0;
|
|
92975
|
+
let references;
|
|
92976
|
+
if (check2 === "fk") {
|
|
92977
|
+
if (columns.length !== 1) {
|
|
92978
|
+
throw new VerifyInputError("--check fk requires exactly one --column (the child FK column).");
|
|
92979
|
+
}
|
|
92980
|
+
references = parseReferences(raw.references);
|
|
92981
|
+
if (hasViolationQuery) {
|
|
92982
|
+
throw new VerifyInputError("--violation-query is only valid with --check custom.");
|
|
92983
|
+
}
|
|
92984
|
+
} else if (check2 === "not-null" || check2 === "unique") {
|
|
92985
|
+
if (columns.length < 1) {
|
|
92986
|
+
throw new VerifyInputError(`--check ${check2} requires at least one --column.`);
|
|
92987
|
+
}
|
|
92988
|
+
if (raw.references !== undefined) {
|
|
92989
|
+
throw new VerifyInputError("--references is only valid with --check fk.");
|
|
92990
|
+
}
|
|
92991
|
+
if (hasViolationQuery) {
|
|
92992
|
+
throw new VerifyInputError("--violation-query is only valid with --check custom.");
|
|
92993
|
+
}
|
|
92994
|
+
} else {
|
|
92995
|
+
if (!hasViolationQuery) {
|
|
92996
|
+
throw new VerifyInputError("--check custom requires --violation-query.");
|
|
92997
|
+
}
|
|
92998
|
+
if (columns.length > 0) {
|
|
92999
|
+
throw new VerifyInputError("--column is not valid with --check custom.");
|
|
93000
|
+
}
|
|
93001
|
+
if (raw.references !== undefined) {
|
|
93002
|
+
throw new VerifyInputError("--references is only valid with --check fk.");
|
|
93003
|
+
}
|
|
93004
|
+
}
|
|
93005
|
+
const subjectNameRaw = raw.subjectName;
|
|
93006
|
+
const summaryRaw = raw.summary;
|
|
93007
|
+
return {
|
|
93008
|
+
table,
|
|
93009
|
+
check: check2,
|
|
93010
|
+
columns,
|
|
93011
|
+
...references ? { references } : {},
|
|
93012
|
+
...check2 === "custom" ? { violationQuery: violationQueryRaw.trim() } : {},
|
|
93013
|
+
allowPreexisting: raw.allowPreexisting === true,
|
|
93014
|
+
baseline: parseBaseline(raw.baseline),
|
|
93015
|
+
afterWrite: raw.afterWrite === true,
|
|
93016
|
+
format,
|
|
93017
|
+
...subjectNameRaw && subjectNameRaw.trim().length > 0 ? { subjectName: subjectNameRaw.trim() } : {},
|
|
93018
|
+
...summaryRaw && summaryRaw.trim().length > 0 ? { summary: summaryRaw.trim() } : {}
|
|
93019
|
+
};
|
|
93020
|
+
}
|
|
93021
|
+
function buildConstraintSubject(input) {
|
|
93022
|
+
return {
|
|
93023
|
+
kind: "table",
|
|
93024
|
+
name: input.subjectName ?? input.table,
|
|
93025
|
+
command: "verify constraint"
|
|
93026
|
+
};
|
|
93027
|
+
}
|
|
93028
|
+
function buildConstraintAfterWriteCommand(input, baseline) {
|
|
93029
|
+
const flags = [`--check ${input.check}`, `--table ${shellQuote2(input.table)}`];
|
|
93030
|
+
if (input.check === "fk") {
|
|
93031
|
+
flags.push(`--column ${shellQuote2(input.columns[0])}`);
|
|
93032
|
+
flags.push(`--references ${shellQuote2(`${input.references.table}.${input.references.column}`)}`);
|
|
93033
|
+
} else if (input.check === "custom") {
|
|
93034
|
+
flags.push(`--violation-query ${shellQuote2(input.violationQuery)}`);
|
|
93035
|
+
} else {
|
|
93036
|
+
for (const c2 of input.columns)
|
|
93037
|
+
flags.push(`--column ${shellQuote2(c2)}`);
|
|
93038
|
+
}
|
|
93039
|
+
if (input.subjectName)
|
|
93040
|
+
flags.push(`--subject-name ${shellQuote2(input.subjectName)}`);
|
|
93041
|
+
if (input.summary)
|
|
93042
|
+
flags.push(`--summary ${shellQuote2(input.summary)}`);
|
|
93043
|
+
if (input.format !== "table")
|
|
93044
|
+
flags.push(`--format ${input.format}`);
|
|
93045
|
+
if (baseline !== undefined && baseline > 0) {
|
|
93046
|
+
flags.push("--allow-preexisting", `--baseline ${baseline}`);
|
|
93047
|
+
}
|
|
93048
|
+
return renderAfterWriteCommand("constraint", flags);
|
|
93049
|
+
}
|
|
93050
|
+
async function runConstraintGuards(runners) {
|
|
93051
|
+
return runGuardSequence([
|
|
93052
|
+
["blacklist", () => runners.blacklistGuard()],
|
|
93053
|
+
["schema", () => runners.schemaGuard()],
|
|
93054
|
+
["violation-query-readonly", () => runners.violationReadonlyGuard()]
|
|
93055
|
+
]);
|
|
93056
|
+
}
|
|
93057
|
+
function constraintDefaultSummary(status2, check2, table) {
|
|
93058
|
+
switch (status2) {
|
|
93059
|
+
case "verified":
|
|
93060
|
+
return `Constraint '${check2}' holds on ${table} (violation count within threshold).`;
|
|
93061
|
+
case "not_verified":
|
|
93062
|
+
return `Constraint '${check2}' is violated on ${table} (violation count exceeds threshold).`;
|
|
93063
|
+
case "blocked":
|
|
93064
|
+
return `Constraint '${check2}' verification was blocked before the violation count on ${table}.`;
|
|
93065
|
+
default:
|
|
93066
|
+
return `Constraint '${check2}' verification could not produce a trustworthy verdict on ${table}.`;
|
|
93067
|
+
}
|
|
93068
|
+
}
|
|
93069
|
+
async function runConstraintPreflight(input, runners) {
|
|
93070
|
+
const guards = await runConstraintGuards(runners);
|
|
93071
|
+
const ready = allGuardsPassed(guards, 3);
|
|
93072
|
+
let baseline;
|
|
93073
|
+
if (ready) {
|
|
93074
|
+
const outcome = await runners.runViolationCount();
|
|
93075
|
+
if (outcome.ran && typeof outcome.count === "number")
|
|
93076
|
+
baseline = outcome.count;
|
|
93077
|
+
}
|
|
93078
|
+
return {
|
|
93079
|
+
scenario: "constraint",
|
|
93080
|
+
mode: "preflight",
|
|
93081
|
+
status: ready ? "ready" : "blocked",
|
|
93082
|
+
check: input.check,
|
|
93083
|
+
table: input.table,
|
|
93084
|
+
violationSql: runners.violationSql,
|
|
93085
|
+
...baseline !== undefined ? { baseline } : {},
|
|
93086
|
+
guards,
|
|
93087
|
+
afterWriteCommand: buildConstraintAfterWriteCommand(input, baseline)
|
|
93088
|
+
};
|
|
93089
|
+
}
|
|
93090
|
+
async function runConstraintAfterWrite(input, runners, clock = {}) {
|
|
93091
|
+
const subject = buildConstraintSubject(input);
|
|
93092
|
+
const guards = await runConstraintGuards(runners);
|
|
93093
|
+
if (!allGuardsPassed(guards, 3)) {
|
|
93094
|
+
const failed = guards.find((g) => g.status === "failed");
|
|
93095
|
+
const blockedReason = failed?.reason ?? "A required guard failed before the violation count.";
|
|
93096
|
+
const artifact3 = buildVerificationArtifact({
|
|
93097
|
+
status: "blocked",
|
|
93098
|
+
subject,
|
|
93099
|
+
summary: input.summary ?? constraintDefaultSummary("blocked", input.check, input.table),
|
|
93100
|
+
evidence: [CONSTRAINT_TASK_PACK_EVIDENCE],
|
|
93101
|
+
blockedReason,
|
|
93102
|
+
now: clock.now,
|
|
93103
|
+
idFactory: clock.idFactory
|
|
93104
|
+
});
|
|
93105
|
+
return {
|
|
93106
|
+
scenario: "constraint",
|
|
93107
|
+
mode: "after-write",
|
|
93108
|
+
status: "blocked",
|
|
93109
|
+
check: input.check,
|
|
93110
|
+
table: input.table,
|
|
93111
|
+
guards,
|
|
93112
|
+
artifact: artifact3,
|
|
93113
|
+
blockedReason
|
|
93114
|
+
};
|
|
93115
|
+
}
|
|
93116
|
+
const outcome = await runners.runViolationCount();
|
|
93117
|
+
const threshold = input.allowPreexisting ? input.baseline : 0;
|
|
93118
|
+
const assertionOutcome = outcome.ran && typeof outcome.count === "number" ? { ran: true, pass: outcome.count <= threshold, auditRef: outcome.auditRef } : { ran: false, reason: outcome.reason, auditRef: outcome.auditRef };
|
|
93119
|
+
const status2 = mapAssertionToStatus(assertionOutcome);
|
|
93120
|
+
const evidence2 = {
|
|
93121
|
+
kind: "assert",
|
|
93122
|
+
command: `constraint:${input.check} <${redactSqlForEvidence(runners.violationSql)}> threshold <=${threshold}`,
|
|
93123
|
+
exitCode: status2 === "verified" ? 0 : 1,
|
|
93124
|
+
...outcome.auditRef ? { auditRef: outcome.auditRef } : {},
|
|
93125
|
+
...status2 === "indeterminate" && outcome.reason ? { note: outcome.reason } : {}
|
|
93126
|
+
};
|
|
93127
|
+
const artifact2 = buildVerificationArtifact({
|
|
93128
|
+
status: status2,
|
|
93129
|
+
subject,
|
|
93130
|
+
summary: input.summary ?? constraintDefaultSummary(status2, input.check, input.table),
|
|
93131
|
+
evidence: [CONSTRAINT_TASK_PACK_EVIDENCE, evidence2],
|
|
93132
|
+
...status2 === "indeterminate" && outcome.reason ? { blockedReason: outcome.reason } : {},
|
|
93133
|
+
now: clock.now,
|
|
93134
|
+
idFactory: clock.idFactory
|
|
93135
|
+
});
|
|
93136
|
+
return {
|
|
93137
|
+
scenario: "constraint",
|
|
93138
|
+
mode: "after-write",
|
|
93139
|
+
status: status2,
|
|
93140
|
+
check: input.check,
|
|
93141
|
+
table: input.table,
|
|
93142
|
+
guards,
|
|
93143
|
+
...outcome.ran && typeof outcome.count === "number" ? { assertion: { violations: outcome.count, threshold, passed: status2 === "verified" } } : {},
|
|
93144
|
+
artifact: artifact2
|
|
93145
|
+
};
|
|
93146
|
+
}
|
|
93147
|
+
var ALLOWED_CONSTRAINT_CHECKS, CONSTRAINT_TASK_PACK_EVIDENCE;
|
|
93148
|
+
var init_constraint = __esm(() => {
|
|
93149
|
+
init_scenario();
|
|
93150
|
+
init_verification();
|
|
93151
|
+
init_scenario();
|
|
93152
|
+
ALLOWED_CONSTRAINT_CHECKS = ["fk", "not-null", "unique", "custom"];
|
|
93153
|
+
CONSTRAINT_TASK_PACK_EVIDENCE = {
|
|
93154
|
+
kind: "task-pack-plan",
|
|
93155
|
+
taskName: "constraint-verify",
|
|
93156
|
+
note: "Preflight guards ran before the read-only violation count."
|
|
93157
|
+
};
|
|
93158
|
+
});
|
|
93159
|
+
|
|
93160
|
+
// src/core/verify/constraint-query.ts
|
|
93161
|
+
function quoteIdent(name2, engine) {
|
|
93162
|
+
const useBacktick = engine === "mysql" || engine === "mariadb";
|
|
93163
|
+
const q = useBacktick ? "`" : '"';
|
|
93164
|
+
const esc = (seg) => `${q}${seg.split(q).join(q + q)}${q}`;
|
|
93165
|
+
return name2.split(".").map((seg) => esc(seg.trim())).join(".");
|
|
93166
|
+
}
|
|
93167
|
+
function buildNotNullViolationQuery(a) {
|
|
93168
|
+
const where = a.columns.map((c2) => `${quoteIdent(c2, a.engine)} IS NULL`).join(" OR ");
|
|
93169
|
+
return `SELECT COUNT(*) AS violation_count FROM ${quoteIdent(a.table, a.engine)} WHERE ${where}`;
|
|
93170
|
+
}
|
|
93171
|
+
function buildUniqueViolationQuery(a) {
|
|
93172
|
+
const cols = a.columns.map((c2) => quoteIdent(c2, a.engine)).join(", ");
|
|
93173
|
+
return `SELECT COUNT(*) AS violation_count FROM (SELECT 1 FROM ${quoteIdent(a.table, a.engine)} GROUP BY ${cols} HAVING COUNT(*) > 1) AS dups`;
|
|
93174
|
+
}
|
|
93175
|
+
function buildFkViolationQuery(a) {
|
|
93176
|
+
const col = quoteIdent(a.column, a.engine);
|
|
93177
|
+
const ref = quoteIdent(a.refColumn, a.engine);
|
|
93178
|
+
return `SELECT COUNT(*) AS violation_count FROM ${quoteIdent(a.table, a.engine)} AS c LEFT JOIN ${quoteIdent(a.refTable, a.engine)} AS p ON c.${col} = p.${ref} WHERE c.${col} IS NOT NULL AND p.${ref} IS NULL`;
|
|
93179
|
+
}
|
|
93180
|
+
function buildViolationQuery(input, engine) {
|
|
93181
|
+
switch (input.check) {
|
|
93182
|
+
case "custom":
|
|
93183
|
+
return input.violationQuery;
|
|
93184
|
+
case "not-null":
|
|
93185
|
+
return buildNotNullViolationQuery({ engine, table: input.table, columns: input.columns });
|
|
93186
|
+
case "unique":
|
|
93187
|
+
return buildUniqueViolationQuery({ engine, table: input.table, columns: input.columns });
|
|
93188
|
+
case "fk":
|
|
93189
|
+
return buildFkViolationQuery({
|
|
93190
|
+
engine,
|
|
93191
|
+
table: input.table,
|
|
93192
|
+
column: input.columns[0],
|
|
93193
|
+
refTable: input.references.table,
|
|
93194
|
+
refColumn: input.references.column
|
|
93195
|
+
});
|
|
93196
|
+
}
|
|
93197
|
+
}
|
|
92933
93198
|
// src/core/verify/index.ts
|
|
92934
93199
|
var init_verify = __esm(() => {
|
|
92935
93200
|
init_scenario();
|
|
92936
93201
|
init_safe_backfill();
|
|
92937
93202
|
init_migration();
|
|
92938
93203
|
init_rollback();
|
|
93204
|
+
init_constraint();
|
|
92939
93205
|
});
|
|
92940
93206
|
|
|
92941
93207
|
// src/commands/verify.ts
|
|
@@ -93197,6 +93463,96 @@ function buildRollbackRunners(ctx, input) {
|
|
|
93197
93463
|
}
|
|
93198
93464
|
};
|
|
93199
93465
|
}
|
|
93466
|
+
function constraintEngineOf(system) {
|
|
93467
|
+
return system;
|
|
93468
|
+
}
|
|
93469
|
+
function buildConstraintRunners(ctx, input) {
|
|
93470
|
+
const { adapter, config } = ctx;
|
|
93471
|
+
const blacklist = config.blacklist ?? { tables: [], columns: {} };
|
|
93472
|
+
const schema = config.schema ?? {};
|
|
93473
|
+
const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
|
|
93474
|
+
const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup });
|
|
93475
|
+
const engine = constraintEngineOf(config.connection.system);
|
|
93476
|
+
const violationSql = buildViolationQuery(input, engine);
|
|
93477
|
+
const columnsExist = async (table, cols) => {
|
|
93478
|
+
const ts = await adapter.getTableSchema(table);
|
|
93479
|
+
const present = new Set(ts.columns.map((c2) => c2.name.toLowerCase()));
|
|
93480
|
+
const missing = cols.filter((c2) => !present.has(c2.trim().toLowerCase()));
|
|
93481
|
+
return missing.length > 0 ? `unknown column(s) on ${table}: ${missing.join(", ")}` : null;
|
|
93482
|
+
};
|
|
93483
|
+
return {
|
|
93484
|
+
violationSql,
|
|
93485
|
+
blacklistGuard: async () => {
|
|
93486
|
+
const bm = new BlacklistManager(config);
|
|
93487
|
+
const targets = [input.table, ...input.references ? [input.references.table] : []];
|
|
93488
|
+
for (const t2 of targets) {
|
|
93489
|
+
if (bm.isTableBlacklisted(t2) && !bm.canOverrideBlacklist()) {
|
|
93490
|
+
return { ok: false, reason: boundedReason(`Target table '${t2}' is blacklisted.`) };
|
|
93491
|
+
}
|
|
93492
|
+
}
|
|
93493
|
+
return { ok: true };
|
|
93494
|
+
},
|
|
93495
|
+
schemaGuard: async () => {
|
|
93496
|
+
try {
|
|
93497
|
+
if (input.check !== "custom") {
|
|
93498
|
+
const miss = await columnsExist(input.table, input.columns);
|
|
93499
|
+
if (miss)
|
|
93500
|
+
return { ok: false, reason: boundedReason(miss) };
|
|
93501
|
+
} else {
|
|
93502
|
+
await adapter.getTableSchema(input.table);
|
|
93503
|
+
}
|
|
93504
|
+
if (input.references) {
|
|
93505
|
+
const refMiss = await columnsExist(input.references.table, [input.references.column]);
|
|
93506
|
+
if (refMiss)
|
|
93507
|
+
return { ok: false, reason: boundedReason(refMiss) };
|
|
93508
|
+
}
|
|
93509
|
+
return { ok: true };
|
|
93510
|
+
} catch (e) {
|
|
93511
|
+
return { ok: false, reason: boundedReason(`schema check failed: ${e.message}`) };
|
|
93512
|
+
}
|
|
93513
|
+
},
|
|
93514
|
+
violationReadonlyGuard: async () => {
|
|
93515
|
+
const r = analyze(violationSql);
|
|
93516
|
+
if (!isPlainSelectVerifyQuery(r.operation, violationSql)) {
|
|
93517
|
+
return {
|
|
93518
|
+
ok: false,
|
|
93519
|
+
reason: boundedReason(`violation query must be a read-only plain SELECT (got ${r.operation}).`)
|
|
93520
|
+
};
|
|
93521
|
+
}
|
|
93522
|
+
return { ok: true };
|
|
93523
|
+
},
|
|
93524
|
+
runViolationCount: async () => {
|
|
93525
|
+
try {
|
|
93526
|
+
const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
|
|
93527
|
+
const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, ctx.options);
|
|
93528
|
+
const result = await executor3.execute(violationSql, { autoLimit: true });
|
|
93529
|
+
const scalar = firstScalar(result);
|
|
93530
|
+
const auditRef = await writeAuditEntry(config, "verify", ctx.options, {
|
|
93531
|
+
success: true,
|
|
93532
|
+
sql: violationSql
|
|
93533
|
+
});
|
|
93534
|
+
if (scalar === null) {
|
|
93535
|
+
return {
|
|
93536
|
+
ran: false,
|
|
93537
|
+
reason: boundedReason("violation query returned no count"),
|
|
93538
|
+
auditRef
|
|
93539
|
+
};
|
|
93540
|
+
}
|
|
93541
|
+
const count = typeof scalar === "number" ? scalar : Number(scalar);
|
|
93542
|
+
if (!Number.isFinite(count)) {
|
|
93543
|
+
return {
|
|
93544
|
+
ran: false,
|
|
93545
|
+
reason: boundedReason(`violation count not numeric: ${scalar}`),
|
|
93546
|
+
auditRef
|
|
93547
|
+
};
|
|
93548
|
+
}
|
|
93549
|
+
return { ran: true, count, auditRef };
|
|
93550
|
+
} catch (e) {
|
|
93551
|
+
return { ran: false, reason: boundedReason(e.message) };
|
|
93552
|
+
}
|
|
93553
|
+
}
|
|
93554
|
+
};
|
|
93555
|
+
}
|
|
93200
93556
|
function formatPreflightTable(r) {
|
|
93201
93557
|
const lines = [
|
|
93202
93558
|
`Scenario: ${r.scenario}`,
|
|
@@ -93405,6 +93761,81 @@ function buildRollbackAfterWriteJson(r, artifactPath) {
|
|
|
93405
93761
|
...r.blockedReason ? { blockedReason: r.blockedReason } : {}
|
|
93406
93762
|
};
|
|
93407
93763
|
}
|
|
93764
|
+
function formatConstraintPreflightTable(r) {
|
|
93765
|
+
const lines = [
|
|
93766
|
+
`Scenario: ${r.scenario}`,
|
|
93767
|
+
`Mode: preflight`,
|
|
93768
|
+
`Check: ${r.check}`,
|
|
93769
|
+
`Table: ${r.table}`,
|
|
93770
|
+
`Status: ${r.status}`,
|
|
93771
|
+
"Guards:"
|
|
93772
|
+
];
|
|
93773
|
+
for (const g of r.guards) {
|
|
93774
|
+
lines.push(` - ${g.name}: ${g.status}${g.reason ? ` (${g.reason})` : ""}`);
|
|
93775
|
+
}
|
|
93776
|
+
if (r.baseline !== undefined)
|
|
93777
|
+
lines.push("", `Baseline violations: ${r.baseline}`);
|
|
93778
|
+
lines.push("", "Violation query (read-only; this command never executes a write):");
|
|
93779
|
+
lines.push(` ${r.violationSql}`);
|
|
93780
|
+
lines.push("", "After-write command (run AFTER you apply your change externally):");
|
|
93781
|
+
lines.push(` ${r.afterWriteCommand}`);
|
|
93782
|
+
lines.push("", "Note: default verdict requires 0 violations; add --allow-preexisting --baseline <N> to tolerate pre-existing ones.");
|
|
93783
|
+
return lines.join(`
|
|
93784
|
+
`);
|
|
93785
|
+
}
|
|
93786
|
+
function formatConstraintAfterWriteTable(r, artifactPath) {
|
|
93787
|
+
const lines = [
|
|
93788
|
+
`Scenario: ${r.scenario}`,
|
|
93789
|
+
`Mode: after-write`,
|
|
93790
|
+
`Check: ${r.check}`,
|
|
93791
|
+
`Table: ${r.table}`,
|
|
93792
|
+
`Status: ${r.status}`
|
|
93793
|
+
];
|
|
93794
|
+
if (r.assertion)
|
|
93795
|
+
lines.push(`Violations: ${r.assertion.violations} (threshold <= ${r.assertion.threshold}) -> ${r.assertion.passed ? "PASS" : "FAIL"}`);
|
|
93796
|
+
if (r.blockedReason)
|
|
93797
|
+
lines.push(`Reason: ${r.blockedReason}`);
|
|
93798
|
+
lines.push(`Summary: ${r.artifact.summary}`);
|
|
93799
|
+
lines.push(`Artifact id: ${r.artifact.id}`);
|
|
93800
|
+
if (artifactPath)
|
|
93801
|
+
lines.push(`Artifact: ${artifactPath}`);
|
|
93802
|
+
lines.push("", `Next: dbcli verification show ${r.artifact.id}`);
|
|
93803
|
+
return lines.join(`
|
|
93804
|
+
`);
|
|
93805
|
+
}
|
|
93806
|
+
function buildConstraintPreflightJson(r) {
|
|
93807
|
+
return {
|
|
93808
|
+
scenario: r.scenario,
|
|
93809
|
+
mode: r.mode,
|
|
93810
|
+
status: r.status,
|
|
93811
|
+
check: r.check,
|
|
93812
|
+
table: r.table,
|
|
93813
|
+
violationSql: r.violationSql,
|
|
93814
|
+
...r.baseline !== undefined ? { baseline: r.baseline } : {},
|
|
93815
|
+
guards: r.guards.map((g) => ({
|
|
93816
|
+
name: g.name,
|
|
93817
|
+
status: g.status,
|
|
93818
|
+
...g.reason ? { reason: g.reason } : {}
|
|
93819
|
+
})),
|
|
93820
|
+
afterWriteCommand: r.afterWriteCommand
|
|
93821
|
+
};
|
|
93822
|
+
}
|
|
93823
|
+
function buildConstraintAfterWriteJson(r, artifactPath) {
|
|
93824
|
+
return {
|
|
93825
|
+
scenario: r.scenario,
|
|
93826
|
+
mode: r.mode,
|
|
93827
|
+
status: r.status,
|
|
93828
|
+
check: r.check,
|
|
93829
|
+
table: r.table,
|
|
93830
|
+
artifact: {
|
|
93831
|
+
id: r.artifact.id,
|
|
93832
|
+
...artifactPath ? { path: artifactPath } : {},
|
|
93833
|
+
subject: r.artifact.subject
|
|
93834
|
+
},
|
|
93835
|
+
...r.assertion ? { assertion: r.assertion } : {},
|
|
93836
|
+
...r.blockedReason ? { blockedReason: r.blockedReason } : {}
|
|
93837
|
+
};
|
|
93838
|
+
}
|
|
93408
93839
|
async function executeScenario(def, options, command) {
|
|
93409
93840
|
let input;
|
|
93410
93841
|
try {
|
|
@@ -93471,7 +93902,7 @@ function registerScenario(parent, def) {
|
|
|
93471
93902
|
def.configureOptions(command);
|
|
93472
93903
|
command.action((options, cmd) => executeScenario(def, options, cmd));
|
|
93473
93904
|
}
|
|
93474
|
-
var SQL_SYSTEMS7, safeBackfillScenario, migrationScenario, rollbackScenario, BUILTIN_VERIFY_SCENARIOS, verifyCommand;
|
|
93905
|
+
var SQL_SYSTEMS7, safeBackfillScenario, migrationScenario, rollbackScenario, constraintScenario, BUILTIN_VERIFY_SCENARIOS, verifyCommand;
|
|
93475
93906
|
var init_verify2 = __esm(() => {
|
|
93476
93907
|
init_esm();
|
|
93477
93908
|
init_adapters();
|
|
@@ -93627,10 +94058,61 @@ var init_verify2 = __esm(() => {
|
|
|
93627
94058
|
return result.status === "verified" && !artifactError;
|
|
93628
94059
|
}
|
|
93629
94060
|
};
|
|
94061
|
+
constraintScenario = {
|
|
94062
|
+
name: "constraint",
|
|
94063
|
+
description: "Preflight or after-write verification that a data-integrity invariant holds across your change (--check fk|not-null|unique|custom); never executes a write",
|
|
94064
|
+
subjectKind: "table",
|
|
94065
|
+
configureOptions(command) {
|
|
94066
|
+
return command.requiredOption("--table <table>", "Table the invariant is checked on").requiredOption("--check <kind>", "Constraint kind: fk | not-null | unique | custom").option("--column <name>", "Column to check (repeatable for not-null/unique; the child FK column for fk)", (val, prev = []) => [...prev, val]).option("--references <table.column>", "Referenced <table>.<column> (required for --check fk)").option("--violation-query <sql>", "Read-only SELECT counting violations (required for --check custom)").option("--allow-preexisting", "Tolerate pre-existing violations: verified when count <= --baseline", false).option("--baseline <n>", "Baseline violation count measured at preflight (use with --allow-preexisting)").option("--after-write", "Re-run the violation count and write a verification artifact", false).option("--format <format>", "Output format: table (default) or json", "table").option("--subject-name <name>", "Optional artifact subject name (default: table)").option("--summary <text>", "Optional artifact summary override (after-write mode)");
|
|
94067
|
+
},
|
|
94068
|
+
normalize(options) {
|
|
94069
|
+
return normalizeConstraintInput({
|
|
94070
|
+
table: options.table,
|
|
94071
|
+
check: options.check,
|
|
94072
|
+
column: options.column,
|
|
94073
|
+
references: options.references,
|
|
94074
|
+
violationQuery: options.violationQuery,
|
|
94075
|
+
allowPreexisting: options.allowPreexisting === true,
|
|
94076
|
+
baseline: options.baseline,
|
|
94077
|
+
afterWrite: options.afterWrite === true,
|
|
94078
|
+
format: options.format,
|
|
94079
|
+
subjectName: options.subjectName,
|
|
94080
|
+
summary: options.summary
|
|
94081
|
+
});
|
|
94082
|
+
},
|
|
94083
|
+
createRunners(context, input) {
|
|
94084
|
+
return buildConstraintRunners(context, input);
|
|
94085
|
+
},
|
|
94086
|
+
runPreflight(input, runners) {
|
|
94087
|
+
return runConstraintPreflight(input, runners);
|
|
94088
|
+
},
|
|
94089
|
+
runAfterWrite(input, runners) {
|
|
94090
|
+
return runConstraintAfterWrite(input, runners);
|
|
94091
|
+
},
|
|
94092
|
+
renderPreflight(result, format) {
|
|
94093
|
+
return format === "json" ? JSON.stringify(buildConstraintPreflightJson(result), null, 2) : formatConstraintPreflightTable(result);
|
|
94094
|
+
},
|
|
94095
|
+
artifactOf(result) {
|
|
94096
|
+
return result.artifact;
|
|
94097
|
+
},
|
|
94098
|
+
afterWriteJson(result, artifactPath) {
|
|
94099
|
+
return buildConstraintAfterWriteJson(result, artifactPath);
|
|
94100
|
+
},
|
|
94101
|
+
renderAfterWriteTable(result, artifactPath) {
|
|
94102
|
+
return formatConstraintAfterWriteTable(result, artifactPath);
|
|
94103
|
+
},
|
|
94104
|
+
isPreflightReady(result) {
|
|
94105
|
+
return result.status === "ready";
|
|
94106
|
+
},
|
|
94107
|
+
isAfterWriteVerified(result, artifactError) {
|
|
94108
|
+
return result.status === "verified" && !artifactError;
|
|
94109
|
+
}
|
|
94110
|
+
};
|
|
93630
94111
|
BUILTIN_VERIFY_SCENARIOS = [
|
|
93631
94112
|
safeBackfillScenario,
|
|
93632
94113
|
migrationScenario,
|
|
93633
|
-
rollbackScenario
|
|
94114
|
+
rollbackScenario,
|
|
94115
|
+
constraintScenario
|
|
93634
94116
|
];
|
|
93635
94117
|
verifyCommand = new Command("verify").description("Run verification scenarios (preflight or after-write). Never executes writes.");
|
|
93636
94118
|
for (const scenario2 of BUILTIN_VERIFY_SCENARIOS) {
|
|
@@ -96730,30 +97212,30 @@ class PostgreSQLDDLGenerator {
|
|
|
96730
97212
|
dropIndex(indexName, _table) {
|
|
96731
97213
|
return { sql: `DROP INDEX ${q(indexName)};`, warnings: [] };
|
|
96732
97214
|
}
|
|
96733
|
-
addConstraint(
|
|
96734
|
-
const t2 = q(
|
|
97215
|
+
addConstraint(constraint2) {
|
|
97216
|
+
const t2 = q(constraint2.table);
|
|
96735
97217
|
const warnings = [];
|
|
96736
|
-
switch (
|
|
97218
|
+
switch (constraint2.type) {
|
|
96737
97219
|
case "foreign_key": {
|
|
96738
|
-
const name2 =
|
|
96739
|
-
const onDelete =
|
|
97220
|
+
const name2 = constraint2.name || `fk_${constraint2.table}_${constraint2.column}`;
|
|
97221
|
+
const onDelete = constraint2.onDelete ? ` ON DELETE ${constraint2.onDelete.toUpperCase().replace("_", " ")}` : "";
|
|
96740
97222
|
return {
|
|
96741
|
-
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} FOREIGN KEY (${q(
|
|
97223
|
+
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} FOREIGN KEY (${q(constraint2.column)}) REFERENCES ${q(constraint2.references.table)}(${q(constraint2.references.column)})${onDelete};`,
|
|
96742
97224
|
warnings
|
|
96743
97225
|
};
|
|
96744
97226
|
}
|
|
96745
97227
|
case "unique": {
|
|
96746
|
-
const name2 =
|
|
96747
|
-
const cols =
|
|
97228
|
+
const name2 = constraint2.name || `uq_${constraint2.table}_${constraint2.columns.join("_")}`;
|
|
97229
|
+
const cols = constraint2.columns.map(q).join(", ");
|
|
96748
97230
|
return {
|
|
96749
97231
|
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} UNIQUE (${cols});`,
|
|
96750
97232
|
warnings
|
|
96751
97233
|
};
|
|
96752
97234
|
}
|
|
96753
97235
|
case "check": {
|
|
96754
|
-
const name2 =
|
|
97236
|
+
const name2 = constraint2.name || `ck_${constraint2.table}`;
|
|
96755
97237
|
return {
|
|
96756
|
-
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} CHECK (${
|
|
97238
|
+
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} CHECK (${constraint2.expression});`,
|
|
96757
97239
|
warnings
|
|
96758
97240
|
};
|
|
96759
97241
|
}
|
|
@@ -96903,30 +97385,30 @@ class MySQLDDLGenerator {
|
|
|
96903
97385
|
warnings: table ? [] : ["MySQL requires ON <table> \u2014 specify table for this operation"]
|
|
96904
97386
|
};
|
|
96905
97387
|
}
|
|
96906
|
-
addConstraint(
|
|
96907
|
-
const t2 = q2(
|
|
97388
|
+
addConstraint(constraint2) {
|
|
97389
|
+
const t2 = q2(constraint2.table);
|
|
96908
97390
|
const warnings = [];
|
|
96909
|
-
switch (
|
|
97391
|
+
switch (constraint2.type) {
|
|
96910
97392
|
case "foreign_key": {
|
|
96911
|
-
const name2 =
|
|
96912
|
-
const onDelete =
|
|
97393
|
+
const name2 = constraint2.name || `fk_${constraint2.table}_${constraint2.column}`;
|
|
97394
|
+
const onDelete = constraint2.onDelete ? ` ON DELETE ${constraint2.onDelete.toUpperCase().replace("_", " ")}` : "";
|
|
96913
97395
|
return {
|
|
96914
|
-
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} FOREIGN KEY (${q2(
|
|
97396
|
+
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} FOREIGN KEY (${q2(constraint2.column)}) REFERENCES ${q2(constraint2.references.table)}(${q2(constraint2.references.column)})${onDelete};`,
|
|
96915
97397
|
warnings
|
|
96916
97398
|
};
|
|
96917
97399
|
}
|
|
96918
97400
|
case "unique": {
|
|
96919
|
-
const name2 =
|
|
96920
|
-
const cols =
|
|
97401
|
+
const name2 = constraint2.name || `uq_${constraint2.table}_${constraint2.columns.join("_")}`;
|
|
97402
|
+
const cols = constraint2.columns.map(q2).join(", ");
|
|
96921
97403
|
return {
|
|
96922
97404
|
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} UNIQUE (${cols});`,
|
|
96923
97405
|
warnings
|
|
96924
97406
|
};
|
|
96925
97407
|
}
|
|
96926
97408
|
case "check": {
|
|
96927
|
-
const name2 =
|
|
97409
|
+
const name2 = constraint2.name || `ck_${constraint2.table}`;
|
|
96928
97410
|
return {
|
|
96929
|
-
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} CHECK (${
|
|
97411
|
+
sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} CHECK (${constraint2.expression});`,
|
|
96930
97412
|
warnings: ["CHECK constraints enforced in MySQL 8.0.16+ and MariaDB 10.2.1+ only"]
|
|
96931
97413
|
};
|
|
96932
97414
|
}
|
package/dist/core.mjs
CHANGED
|
@@ -13537,7 +13537,7 @@ var ENGINE_CAPABILITIES = Object.freeze({
|
|
|
13537
13537
|
queries: cap("limited", "local-write", "Snippet management works with Redis-specific saved-query limitations."),
|
|
13538
13538
|
insert: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
|
|
13539
13539
|
update: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
|
|
13540
|
-
delete: cap("
|
|
13540
|
+
delete: cap("limited", "db-write", "Basic delete via DEL/HDEL/LREM/SREM/ZREM (needs data-admin); supports --dry-run."),
|
|
13541
13541
|
export: cap("unsupported", "none", "Redis export is not supported."),
|
|
13542
13542
|
blacklist: cap("limited", "local-write", "Key-glob rejection (Redis-native pattern) plus value/hash-field masking ([REDACTED])."),
|
|
13543
13543
|
check: cap("unsupported", "none", "Data health check is SQL-only."),
|