@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/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.37.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("unsupported", "none", "Dedicated write subcommand is not exposed."),
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(constraint) {
96734
- const t2 = q(constraint.table);
97215
+ addConstraint(constraint2) {
97216
+ const t2 = q(constraint2.table);
96735
97217
  const warnings = [];
96736
- switch (constraint.type) {
97218
+ switch (constraint2.type) {
96737
97219
  case "foreign_key": {
96738
- const name2 = constraint.name || `fk_${constraint.table}_${constraint.column}`;
96739
- const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
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(constraint.column)}) REFERENCES ${q(constraint.references.table)}(${q(constraint.references.column)})${onDelete};`,
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 = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
96747
- const cols = constraint.columns.map(q).join(", ");
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 = constraint.name || `ck_${constraint.table}`;
97236
+ const name2 = constraint2.name || `ck_${constraint2.table}`;
96755
97237
  return {
96756
- sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} CHECK (${constraint.expression});`,
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(constraint) {
96907
- const t2 = q2(constraint.table);
97388
+ addConstraint(constraint2) {
97389
+ const t2 = q2(constraint2.table);
96908
97390
  const warnings = [];
96909
- switch (constraint.type) {
97391
+ switch (constraint2.type) {
96910
97392
  case "foreign_key": {
96911
- const name2 = constraint.name || `fk_${constraint.table}_${constraint.column}`;
96912
- const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
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(constraint.column)}) REFERENCES ${q2(constraint.references.table)}(${q2(constraint.references.column)})${onDelete};`,
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 = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
96920
- const cols = constraint.columns.map(q2).join(", ");
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 = constraint.name || `ck_${constraint.table}`;
97409
+ const name2 = constraint2.name || `ck_${constraint2.table}`;
96928
97410
  return {
96929
- sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} CHECK (${constraint.expression});`,
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("unsupported", "none", "Dedicated write subcommand is not exposed."),
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."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carllee1983/dbcli",
3
- "version": "1.37.1",
3
+ "version": "1.38.1",
4
4
  "description": "Database CLI for AI agents",
5
5
  "type": "module",
6
6
  "publishConfig": {