@carllee1983/dbcli 1.37.1 → 1.39.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.39.1",
56
56
  description: "Database CLI for AI agents",
57
57
  type: "module",
58
58
  publishConfig: {
@@ -8809,8 +8809,9 @@ class BlacklistManager {
8809
8809
  }
8810
8810
 
8811
8811
  // src/core/saved-queries/types.ts
8812
- var SavedQueryError;
8812
+ var SUPPORTED_CHART_TYPES, SavedQueryError;
8813
8813
  var init_types2 = __esm(() => {
8814
+ SUPPORTED_CHART_TYPES = ["line", "bar", "area", "pie"];
8814
8815
  SavedQueryError = class SavedQueryError extends Error {
8815
8816
  code;
8816
8817
  file;
@@ -9577,7 +9578,7 @@ function parseFrontmatter(yaml, input) {
9577
9578
  const tags = Array.isArray(raw.tags) ? raw.tags.map(String) : [];
9578
9579
  const index = typeof raw.index === "string" ? raw.index : undefined;
9579
9580
  const intent = normaliseIntent(raw.intent, input);
9580
- const visual = normaliseVisual(raw.visual);
9581
+ const visual = normaliseVisual(raw.visual, input);
9581
9582
  const target = typeof raw.target === "string" ? raw.target : undefined;
9582
9583
  const operation = raw.operation === "find" || raw.operation === "aggregate" ? raw.operation : undefined;
9583
9584
  const verify = normaliseVerify(raw.verify, input);
@@ -9599,7 +9600,7 @@ function parseFrontmatter(yaml, input) {
9599
9600
  warnings
9600
9601
  };
9601
9602
  }
9602
- function normaliseVisual(value) {
9603
+ function normaliseVisual(value, input) {
9603
9604
  if (value === undefined || value === null || typeof value !== "object")
9604
9605
  return;
9605
9606
  const raw = value;
@@ -9625,6 +9626,9 @@ function normaliseVisual(value) {
9625
9626
  if (typeof item === "object" && item !== null) {
9626
9627
  const c2 = item;
9627
9628
  if (typeof c2.type === "string" && typeof c2.x === "string" && Array.isArray(c2.y)) {
9629
+ if (!SUPPORTED_CHART_TYPES.includes(c2.type)) {
9630
+ throw new SavedQueryError(`Snippet '${input.key}' has invalid chart type '${c2.type}' (supported: ${SUPPORTED_CHART_TYPES.join(", ")})`, "PARSE_ERROR", input.file);
9631
+ }
9628
9632
  charts.push({
9629
9633
  type: c2.type,
9630
9634
  title: typeof c2.title === "string" ? c2.title : undefined,
@@ -23087,7 +23091,7 @@ var init_capabilities = __esm(() => {
23087
23091
  queries: cap("limited", "local-write", "Snippet management works with Redis-specific saved-query limitations."),
23088
23092
  insert: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
23089
23093
  update: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
23090
- delete: cap("unsupported", "none", "Dedicated write subcommand is not exposed."),
23094
+ delete: cap("limited", "db-write", "Basic delete via DEL/HDEL/LREM/SREM/ZREM (needs data-admin); supports --dry-run."),
23091
23095
  export: cap("unsupported", "none", "Redis export is not supported."),
23092
23096
  blacklist: cap("limited", "local-write", "Key-glob rejection (Redis-native pattern) plus value/hash-field masking ([REDACTED])."),
23093
23097
  check: cap("unsupported", "none", "Data health check is SQL-only."),
@@ -24863,6 +24867,17 @@ class RedisAdapter {
24863
24867
  const regexes = rules.map((p) => globToRegex(p));
24864
24868
  return keys.filter((k) => !regexes.some((r) => r.test(k))).map((name2) => ({ name: name2 }));
24865
24869
  }
24870
+ async sampleKeyNames(limit) {
24871
+ const client = this.requireClient();
24872
+ const keys = await scanAllKeys(client, "*", limit, limit);
24873
+ const truncated = keys.length >= limit;
24874
+ const rules = this.blacklistRules;
24875
+ if (rules.length === 0)
24876
+ return { names: keys, truncated };
24877
+ const regexes = rules.map((p) => globToRegex(p));
24878
+ const names = keys.filter((k) => !regexes.some((r) => r.test(k)));
24879
+ return { names, truncated };
24880
+ }
24866
24881
  async getDbSize() {
24867
24882
  const client = this.requireClient();
24868
24883
  const reply = await client.send("DBSIZE", []);
@@ -25145,7 +25160,7 @@ function parseRedisCommand(input) {
25145
25160
  push();
25146
25161
  return tokens;
25147
25162
  }
25148
- async function scanAllKeys(client, pattern, count) {
25163
+ async function scanAllKeys(client, pattern, count, maxKeys = 1e5) {
25149
25164
  const seen = new Set;
25150
25165
  let cursor = "0";
25151
25166
  do {
@@ -25160,7 +25175,7 @@ async function scanAllKeys(client, pattern, count) {
25160
25175
  for (const k of batch)
25161
25176
  seen.add(k);
25162
25177
  cursor = next;
25163
- if (seen.size >= 1e5)
25178
+ if (seen.size >= maxKeys)
25164
25179
  break;
25165
25180
  } while (cursor !== "0");
25166
25181
  return Array.from(seen);
@@ -28927,6 +28942,7 @@ var init_assert_artifact = __esm(() => {
28927
28942
  "assertion",
28928
28943
  "migration",
28929
28944
  "backfill",
28945
+ "table",
28930
28946
  "manual"
28931
28947
  ];
28932
28948
  AssertArtifactError = class AssertArtifactError extends Error {
@@ -92930,12 +92946,277 @@ var init_rollback = __esm(() => {
92930
92946
  note: "Preflight guards ran before post-rollback read-back verification."
92931
92947
  };
92932
92948
  });
92949
+
92950
+ // src/core/verify/constraint.ts
92951
+ function normalizeConstraintCheck(raw) {
92952
+ const check2 = raw ?? "";
92953
+ if (!ALLOWED_CONSTRAINT_CHECKS.includes(check2)) {
92954
+ throw new VerifyInputError(`Invalid --check '${check2}'. Allowed: ${ALLOWED_CONSTRAINT_CHECKS.join(", ")}`);
92955
+ }
92956
+ return check2;
92957
+ }
92958
+ function toColumns(raw) {
92959
+ if (raw === undefined || raw === null)
92960
+ return [];
92961
+ const arr = Array.isArray(raw) ? raw : [raw];
92962
+ return arr.map((c2) => requireNonEmpty(c2, "--column"));
92963
+ }
92964
+ function parseReferences(raw) {
92965
+ const ref = requireNonEmpty(raw, "--references");
92966
+ const parts = ref.split(".").map((p) => p.trim());
92967
+ if (parts.length < 2 || parts.some((p) => p.length === 0)) {
92968
+ throw new VerifyInputError(`--references must be '<table>.<column>' (got '${ref}')`);
92969
+ }
92970
+ const column = parts.pop();
92971
+ const table = parts.join(".");
92972
+ return { table, column };
92973
+ }
92974
+ function parseBaseline(raw) {
92975
+ if (raw === undefined || raw === null || raw === "")
92976
+ return 0;
92977
+ const n = Number(raw);
92978
+ if (!Number.isInteger(n) || n < 0) {
92979
+ throw new VerifyInputError(`--baseline must be a non-negative integer (got '${String(raw)}')`);
92980
+ }
92981
+ return n;
92982
+ }
92983
+ function normalizeConstraintInput(raw) {
92984
+ const check2 = normalizeConstraintCheck(raw.check);
92985
+ const table = requireNonEmpty(raw.table, "--table");
92986
+ const format = normalizeFormat(raw.format);
92987
+ const columns = toColumns(raw.column);
92988
+ const violationQueryRaw = raw.violationQuery;
92989
+ const hasViolationQuery = typeof violationQueryRaw === "string" && violationQueryRaw.trim().length > 0;
92990
+ let references;
92991
+ if (check2 === "fk") {
92992
+ if (columns.length !== 1) {
92993
+ throw new VerifyInputError("--check fk requires exactly one --column (the child FK column).");
92994
+ }
92995
+ references = parseReferences(raw.references);
92996
+ if (hasViolationQuery) {
92997
+ throw new VerifyInputError("--violation-query is only valid with --check custom.");
92998
+ }
92999
+ } else if (check2 === "not-null" || check2 === "unique") {
93000
+ if (columns.length < 1) {
93001
+ throw new VerifyInputError(`--check ${check2} requires at least one --column.`);
93002
+ }
93003
+ if (raw.references !== undefined) {
93004
+ throw new VerifyInputError("--references is only valid with --check fk.");
93005
+ }
93006
+ if (hasViolationQuery) {
93007
+ throw new VerifyInputError("--violation-query is only valid with --check custom.");
93008
+ }
93009
+ } else {
93010
+ if (!hasViolationQuery) {
93011
+ throw new VerifyInputError("--check custom requires --violation-query.");
93012
+ }
93013
+ if (columns.length > 0) {
93014
+ throw new VerifyInputError("--column is not valid with --check custom.");
93015
+ }
93016
+ if (raw.references !== undefined) {
93017
+ throw new VerifyInputError("--references is only valid with --check fk.");
93018
+ }
93019
+ }
93020
+ const subjectNameRaw = raw.subjectName;
93021
+ const summaryRaw = raw.summary;
93022
+ return {
93023
+ table,
93024
+ check: check2,
93025
+ columns,
93026
+ ...references ? { references } : {},
93027
+ ...check2 === "custom" ? { violationQuery: violationQueryRaw.trim() } : {},
93028
+ allowPreexisting: raw.allowPreexisting === true,
93029
+ baseline: parseBaseline(raw.baseline),
93030
+ afterWrite: raw.afterWrite === true,
93031
+ format,
93032
+ ...subjectNameRaw && subjectNameRaw.trim().length > 0 ? { subjectName: subjectNameRaw.trim() } : {},
93033
+ ...summaryRaw && summaryRaw.trim().length > 0 ? { summary: summaryRaw.trim() } : {}
93034
+ };
93035
+ }
93036
+ function buildConstraintSubject(input) {
93037
+ return {
93038
+ kind: "table",
93039
+ name: input.subjectName ?? input.table,
93040
+ command: "verify constraint"
93041
+ };
93042
+ }
93043
+ function buildConstraintAfterWriteCommand(input, baseline) {
93044
+ const flags = [`--check ${input.check}`, `--table ${shellQuote2(input.table)}`];
93045
+ if (input.check === "fk") {
93046
+ flags.push(`--column ${shellQuote2(input.columns[0])}`);
93047
+ flags.push(`--references ${shellQuote2(`${input.references.table}.${input.references.column}`)}`);
93048
+ } else if (input.check === "custom") {
93049
+ flags.push(`--violation-query ${shellQuote2(input.violationQuery)}`);
93050
+ } else {
93051
+ for (const c2 of input.columns)
93052
+ flags.push(`--column ${shellQuote2(c2)}`);
93053
+ }
93054
+ if (input.subjectName)
93055
+ flags.push(`--subject-name ${shellQuote2(input.subjectName)}`);
93056
+ if (input.summary)
93057
+ flags.push(`--summary ${shellQuote2(input.summary)}`);
93058
+ if (input.format !== "table")
93059
+ flags.push(`--format ${input.format}`);
93060
+ if (baseline !== undefined && baseline > 0) {
93061
+ flags.push("--allow-preexisting", `--baseline ${baseline}`);
93062
+ }
93063
+ return renderAfterWriteCommand("constraint", flags);
93064
+ }
93065
+ async function runConstraintGuards(runners) {
93066
+ return runGuardSequence([
93067
+ ["blacklist", () => runners.blacklistGuard()],
93068
+ ["schema", () => runners.schemaGuard()],
93069
+ ["violation-query-readonly", () => runners.violationReadonlyGuard()]
93070
+ ]);
93071
+ }
93072
+ function constraintDefaultSummary(status2, check2, table) {
93073
+ switch (status2) {
93074
+ case "verified":
93075
+ return `Constraint '${check2}' holds on ${table} (violation count within threshold).`;
93076
+ case "not_verified":
93077
+ return `Constraint '${check2}' is violated on ${table} (violation count exceeds threshold).`;
93078
+ case "blocked":
93079
+ return `Constraint '${check2}' verification was blocked before the violation count on ${table}.`;
93080
+ default:
93081
+ return `Constraint '${check2}' verification could not produce a trustworthy verdict on ${table}.`;
93082
+ }
93083
+ }
93084
+ async function runConstraintPreflight(input, runners) {
93085
+ const guards = await runConstraintGuards(runners);
93086
+ const ready = allGuardsPassed(guards, 3);
93087
+ let baseline;
93088
+ if (ready) {
93089
+ const outcome = await runners.runViolationCount();
93090
+ if (outcome.ran && typeof outcome.count === "number")
93091
+ baseline = outcome.count;
93092
+ }
93093
+ return {
93094
+ scenario: "constraint",
93095
+ mode: "preflight",
93096
+ status: ready ? "ready" : "blocked",
93097
+ check: input.check,
93098
+ table: input.table,
93099
+ violationSql: runners.violationSql,
93100
+ ...baseline !== undefined ? { baseline } : {},
93101
+ guards,
93102
+ afterWriteCommand: buildConstraintAfterWriteCommand(input, baseline)
93103
+ };
93104
+ }
93105
+ async function runConstraintAfterWrite(input, runners, clock = {}) {
93106
+ const subject = buildConstraintSubject(input);
93107
+ const guards = await runConstraintGuards(runners);
93108
+ if (!allGuardsPassed(guards, 3)) {
93109
+ const failed = guards.find((g) => g.status === "failed");
93110
+ const blockedReason = failed?.reason ?? "A required guard failed before the violation count.";
93111
+ const artifact3 = buildVerificationArtifact({
93112
+ status: "blocked",
93113
+ subject,
93114
+ summary: input.summary ?? constraintDefaultSummary("blocked", input.check, input.table),
93115
+ evidence: [CONSTRAINT_TASK_PACK_EVIDENCE],
93116
+ blockedReason,
93117
+ now: clock.now,
93118
+ idFactory: clock.idFactory
93119
+ });
93120
+ return {
93121
+ scenario: "constraint",
93122
+ mode: "after-write",
93123
+ status: "blocked",
93124
+ check: input.check,
93125
+ table: input.table,
93126
+ guards,
93127
+ artifact: artifact3,
93128
+ blockedReason
93129
+ };
93130
+ }
93131
+ const outcome = await runners.runViolationCount();
93132
+ const threshold = input.allowPreexisting ? input.baseline : 0;
93133
+ 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 };
93134
+ const status2 = mapAssertionToStatus(assertionOutcome);
93135
+ const evidence2 = {
93136
+ kind: "assert",
93137
+ command: `constraint:${input.check} <${redactSqlForEvidence(runners.violationSql)}> threshold <=${threshold}`,
93138
+ exitCode: status2 === "verified" ? 0 : 1,
93139
+ ...outcome.auditRef ? { auditRef: outcome.auditRef } : {},
93140
+ ...status2 === "indeterminate" && outcome.reason ? { note: outcome.reason } : {}
93141
+ };
93142
+ const artifact2 = buildVerificationArtifact({
93143
+ status: status2,
93144
+ subject,
93145
+ summary: input.summary ?? constraintDefaultSummary(status2, input.check, input.table),
93146
+ evidence: [CONSTRAINT_TASK_PACK_EVIDENCE, evidence2],
93147
+ ...status2 === "indeterminate" && outcome.reason ? { blockedReason: outcome.reason } : {},
93148
+ now: clock.now,
93149
+ idFactory: clock.idFactory
93150
+ });
93151
+ return {
93152
+ scenario: "constraint",
93153
+ mode: "after-write",
93154
+ status: status2,
93155
+ check: input.check,
93156
+ table: input.table,
93157
+ guards,
93158
+ ...outcome.ran && typeof outcome.count === "number" ? { assertion: { violations: outcome.count, threshold, passed: status2 === "verified" } } : {},
93159
+ artifact: artifact2
93160
+ };
93161
+ }
93162
+ var ALLOWED_CONSTRAINT_CHECKS, CONSTRAINT_TASK_PACK_EVIDENCE;
93163
+ var init_constraint = __esm(() => {
93164
+ init_scenario();
93165
+ init_verification();
93166
+ init_scenario();
93167
+ ALLOWED_CONSTRAINT_CHECKS = ["fk", "not-null", "unique", "custom"];
93168
+ CONSTRAINT_TASK_PACK_EVIDENCE = {
93169
+ kind: "task-pack-plan",
93170
+ taskName: "constraint-verify",
93171
+ note: "Preflight guards ran before the read-only violation count."
93172
+ };
93173
+ });
93174
+
93175
+ // src/core/verify/constraint-query.ts
93176
+ function quoteIdent(name2, engine) {
93177
+ const useBacktick = engine === "mysql" || engine === "mariadb";
93178
+ const q = useBacktick ? "`" : '"';
93179
+ const esc = (seg) => `${q}${seg.split(q).join(q + q)}${q}`;
93180
+ return name2.split(".").map((seg) => esc(seg.trim())).join(".");
93181
+ }
93182
+ function buildNotNullViolationQuery(a) {
93183
+ const where = a.columns.map((c2) => `${quoteIdent(c2, a.engine)} IS NULL`).join(" OR ");
93184
+ return `SELECT COUNT(*) AS violation_count FROM ${quoteIdent(a.table, a.engine)} WHERE ${where}`;
93185
+ }
93186
+ function buildUniqueViolationQuery(a) {
93187
+ const cols = a.columns.map((c2) => quoteIdent(c2, a.engine)).join(", ");
93188
+ return `SELECT COUNT(*) AS violation_count FROM (SELECT 1 FROM ${quoteIdent(a.table, a.engine)} GROUP BY ${cols} HAVING COUNT(*) > 1) AS dups`;
93189
+ }
93190
+ function buildFkViolationQuery(a) {
93191
+ const col = quoteIdent(a.column, a.engine);
93192
+ const ref = quoteIdent(a.refColumn, a.engine);
93193
+ 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`;
93194
+ }
93195
+ function buildViolationQuery(input, engine) {
93196
+ switch (input.check) {
93197
+ case "custom":
93198
+ return input.violationQuery;
93199
+ case "not-null":
93200
+ return buildNotNullViolationQuery({ engine, table: input.table, columns: input.columns });
93201
+ case "unique":
93202
+ return buildUniqueViolationQuery({ engine, table: input.table, columns: input.columns });
93203
+ case "fk":
93204
+ return buildFkViolationQuery({
93205
+ engine,
93206
+ table: input.table,
93207
+ column: input.columns[0],
93208
+ refTable: input.references.table,
93209
+ refColumn: input.references.column
93210
+ });
93211
+ }
93212
+ }
92933
93213
  // src/core/verify/index.ts
92934
93214
  var init_verify = __esm(() => {
92935
93215
  init_scenario();
92936
93216
  init_safe_backfill();
92937
93217
  init_migration();
92938
93218
  init_rollback();
93219
+ init_constraint();
92939
93220
  });
92940
93221
 
92941
93222
  // src/commands/verify.ts
@@ -93197,6 +93478,96 @@ function buildRollbackRunners(ctx, input) {
93197
93478
  }
93198
93479
  };
93199
93480
  }
93481
+ function constraintEngineOf(system) {
93482
+ return system;
93483
+ }
93484
+ function buildConstraintRunners(ctx, input) {
93485
+ const { adapter, config } = ctx;
93486
+ const blacklist = config.blacklist ?? { tables: [], columns: {} };
93487
+ const schema = config.schema ?? {};
93488
+ const schemaLookup = { tables: schema, cacheAvailable: Object.keys(schema).length > 0 };
93489
+ const analyze = (sql) => analyzeQueryRisk({ sql: sql.trim(), permission: config.permission, blacklist, schemaLookup });
93490
+ const engine = constraintEngineOf(config.connection.system);
93491
+ const violationSql = buildViolationQuery(input, engine);
93492
+ const columnsExist = async (table, cols) => {
93493
+ const ts = await adapter.getTableSchema(table);
93494
+ const present = new Set(ts.columns.map((c2) => c2.name.toLowerCase()));
93495
+ const missing = cols.filter((c2) => !present.has(c2.trim().toLowerCase()));
93496
+ return missing.length > 0 ? `unknown column(s) on ${table}: ${missing.join(", ")}` : null;
93497
+ };
93498
+ return {
93499
+ violationSql,
93500
+ blacklistGuard: async () => {
93501
+ const bm = new BlacklistManager(config);
93502
+ const targets = [input.table, ...input.references ? [input.references.table] : []];
93503
+ for (const t2 of targets) {
93504
+ if (bm.isTableBlacklisted(t2) && !bm.canOverrideBlacklist()) {
93505
+ return { ok: false, reason: boundedReason(`Target table '${t2}' is blacklisted.`) };
93506
+ }
93507
+ }
93508
+ return { ok: true };
93509
+ },
93510
+ schemaGuard: async () => {
93511
+ try {
93512
+ if (input.check !== "custom") {
93513
+ const miss = await columnsExist(input.table, input.columns);
93514
+ if (miss)
93515
+ return { ok: false, reason: boundedReason(miss) };
93516
+ } else {
93517
+ await adapter.getTableSchema(input.table);
93518
+ }
93519
+ if (input.references) {
93520
+ const refMiss = await columnsExist(input.references.table, [input.references.column]);
93521
+ if (refMiss)
93522
+ return { ok: false, reason: boundedReason(refMiss) };
93523
+ }
93524
+ return { ok: true };
93525
+ } catch (e) {
93526
+ return { ok: false, reason: boundedReason(`schema check failed: ${e.message}`) };
93527
+ }
93528
+ },
93529
+ violationReadonlyGuard: async () => {
93530
+ const r = analyze(violationSql);
93531
+ if (!isPlainSelectVerifyQuery(r.operation, violationSql)) {
93532
+ return {
93533
+ ok: false,
93534
+ reason: boundedReason(`violation query must be a read-only plain SELECT (got ${r.operation}).`)
93535
+ };
93536
+ }
93537
+ return { ok: true };
93538
+ },
93539
+ runViolationCount: async () => {
93540
+ try {
93541
+ const blacklistValidator = new BlacklistValidator(new BlacklistManager(config));
93542
+ const executor3 = new QueryExecutor(adapter, config.permission, blacklistValidator, config, ctx.options);
93543
+ const result = await executor3.execute(violationSql, { autoLimit: true });
93544
+ const scalar = firstScalar(result);
93545
+ const auditRef = await writeAuditEntry(config, "verify", ctx.options, {
93546
+ success: true,
93547
+ sql: violationSql
93548
+ });
93549
+ if (scalar === null) {
93550
+ return {
93551
+ ran: false,
93552
+ reason: boundedReason("violation query returned no count"),
93553
+ auditRef
93554
+ };
93555
+ }
93556
+ const count = typeof scalar === "number" ? scalar : Number(scalar);
93557
+ if (!Number.isFinite(count)) {
93558
+ return {
93559
+ ran: false,
93560
+ reason: boundedReason(`violation count not numeric: ${scalar}`),
93561
+ auditRef
93562
+ };
93563
+ }
93564
+ return { ran: true, count, auditRef };
93565
+ } catch (e) {
93566
+ return { ran: false, reason: boundedReason(e.message) };
93567
+ }
93568
+ }
93569
+ };
93570
+ }
93200
93571
  function formatPreflightTable(r) {
93201
93572
  const lines = [
93202
93573
  `Scenario: ${r.scenario}`,
@@ -93405,6 +93776,81 @@ function buildRollbackAfterWriteJson(r, artifactPath) {
93405
93776
  ...r.blockedReason ? { blockedReason: r.blockedReason } : {}
93406
93777
  };
93407
93778
  }
93779
+ function formatConstraintPreflightTable(r) {
93780
+ const lines = [
93781
+ `Scenario: ${r.scenario}`,
93782
+ `Mode: preflight`,
93783
+ `Check: ${r.check}`,
93784
+ `Table: ${r.table}`,
93785
+ `Status: ${r.status}`,
93786
+ "Guards:"
93787
+ ];
93788
+ for (const g of r.guards) {
93789
+ lines.push(` - ${g.name}: ${g.status}${g.reason ? ` (${g.reason})` : ""}`);
93790
+ }
93791
+ if (r.baseline !== undefined)
93792
+ lines.push("", `Baseline violations: ${r.baseline}`);
93793
+ lines.push("", "Violation query (read-only; this command never executes a write):");
93794
+ lines.push(` ${r.violationSql}`);
93795
+ lines.push("", "After-write command (run AFTER you apply your change externally):");
93796
+ lines.push(` ${r.afterWriteCommand}`);
93797
+ lines.push("", "Note: default verdict requires 0 violations; add --allow-preexisting --baseline <N> to tolerate pre-existing ones.");
93798
+ return lines.join(`
93799
+ `);
93800
+ }
93801
+ function formatConstraintAfterWriteTable(r, artifactPath) {
93802
+ const lines = [
93803
+ `Scenario: ${r.scenario}`,
93804
+ `Mode: after-write`,
93805
+ `Check: ${r.check}`,
93806
+ `Table: ${r.table}`,
93807
+ `Status: ${r.status}`
93808
+ ];
93809
+ if (r.assertion)
93810
+ lines.push(`Violations: ${r.assertion.violations} (threshold <= ${r.assertion.threshold}) -> ${r.assertion.passed ? "PASS" : "FAIL"}`);
93811
+ if (r.blockedReason)
93812
+ lines.push(`Reason: ${r.blockedReason}`);
93813
+ lines.push(`Summary: ${r.artifact.summary}`);
93814
+ lines.push(`Artifact id: ${r.artifact.id}`);
93815
+ if (artifactPath)
93816
+ lines.push(`Artifact: ${artifactPath}`);
93817
+ lines.push("", `Next: dbcli verification show ${r.artifact.id}`);
93818
+ return lines.join(`
93819
+ `);
93820
+ }
93821
+ function buildConstraintPreflightJson(r) {
93822
+ return {
93823
+ scenario: r.scenario,
93824
+ mode: r.mode,
93825
+ status: r.status,
93826
+ check: r.check,
93827
+ table: r.table,
93828
+ violationSql: r.violationSql,
93829
+ ...r.baseline !== undefined ? { baseline: r.baseline } : {},
93830
+ guards: r.guards.map((g) => ({
93831
+ name: g.name,
93832
+ status: g.status,
93833
+ ...g.reason ? { reason: g.reason } : {}
93834
+ })),
93835
+ afterWriteCommand: r.afterWriteCommand
93836
+ };
93837
+ }
93838
+ function buildConstraintAfterWriteJson(r, artifactPath) {
93839
+ return {
93840
+ scenario: r.scenario,
93841
+ mode: r.mode,
93842
+ status: r.status,
93843
+ check: r.check,
93844
+ table: r.table,
93845
+ artifact: {
93846
+ id: r.artifact.id,
93847
+ ...artifactPath ? { path: artifactPath } : {},
93848
+ subject: r.artifact.subject
93849
+ },
93850
+ ...r.assertion ? { assertion: r.assertion } : {},
93851
+ ...r.blockedReason ? { blockedReason: r.blockedReason } : {}
93852
+ };
93853
+ }
93408
93854
  async function executeScenario(def, options, command) {
93409
93855
  let input;
93410
93856
  try {
@@ -93471,7 +93917,7 @@ function registerScenario(parent, def) {
93471
93917
  def.configureOptions(command);
93472
93918
  command.action((options, cmd) => executeScenario(def, options, cmd));
93473
93919
  }
93474
- var SQL_SYSTEMS7, safeBackfillScenario, migrationScenario, rollbackScenario, BUILTIN_VERIFY_SCENARIOS, verifyCommand;
93920
+ var SQL_SYSTEMS7, safeBackfillScenario, migrationScenario, rollbackScenario, constraintScenario, BUILTIN_VERIFY_SCENARIOS, verifyCommand;
93475
93921
  var init_verify2 = __esm(() => {
93476
93922
  init_esm();
93477
93923
  init_adapters();
@@ -93627,10 +94073,61 @@ var init_verify2 = __esm(() => {
93627
94073
  return result.status === "verified" && !artifactError;
93628
94074
  }
93629
94075
  };
94076
+ constraintScenario = {
94077
+ name: "constraint",
94078
+ 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",
94079
+ subjectKind: "table",
94080
+ configureOptions(command) {
94081
+ 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)");
94082
+ },
94083
+ normalize(options) {
94084
+ return normalizeConstraintInput({
94085
+ table: options.table,
94086
+ check: options.check,
94087
+ column: options.column,
94088
+ references: options.references,
94089
+ violationQuery: options.violationQuery,
94090
+ allowPreexisting: options.allowPreexisting === true,
94091
+ baseline: options.baseline,
94092
+ afterWrite: options.afterWrite === true,
94093
+ format: options.format,
94094
+ subjectName: options.subjectName,
94095
+ summary: options.summary
94096
+ });
94097
+ },
94098
+ createRunners(context, input) {
94099
+ return buildConstraintRunners(context, input);
94100
+ },
94101
+ runPreflight(input, runners) {
94102
+ return runConstraintPreflight(input, runners);
94103
+ },
94104
+ runAfterWrite(input, runners) {
94105
+ return runConstraintAfterWrite(input, runners);
94106
+ },
94107
+ renderPreflight(result, format) {
94108
+ return format === "json" ? JSON.stringify(buildConstraintPreflightJson(result), null, 2) : formatConstraintPreflightTable(result);
94109
+ },
94110
+ artifactOf(result) {
94111
+ return result.artifact;
94112
+ },
94113
+ afterWriteJson(result, artifactPath) {
94114
+ return buildConstraintAfterWriteJson(result, artifactPath);
94115
+ },
94116
+ renderAfterWriteTable(result, artifactPath) {
94117
+ return formatConstraintAfterWriteTable(result, artifactPath);
94118
+ },
94119
+ isPreflightReady(result) {
94120
+ return result.status === "ready";
94121
+ },
94122
+ isAfterWriteVerified(result, artifactError) {
94123
+ return result.status === "verified" && !artifactError;
94124
+ }
94125
+ };
93630
94126
  BUILTIN_VERIFY_SCENARIOS = [
93631
94127
  safeBackfillScenario,
93632
94128
  migrationScenario,
93633
- rollbackScenario
94129
+ rollbackScenario,
94130
+ constraintScenario
93634
94131
  ];
93635
94132
  verifyCommand = new Command("verify").description("Run verification scenarios (preflight or after-write). Never executes writes.");
93636
94133
  for (const scenario2 of BUILTIN_VERIFY_SCENARIOS) {
@@ -95813,32 +96310,6 @@ var init_meta_commands = __esm(() => {
95813
96310
  VALID_FORMATS = ["table", "json", "csv"];
95814
96311
  });
95815
96312
 
95816
- // src/core/repl/command-registry.ts
95817
- var exports_command_registry = {};
95818
- __export(exports_command_registry, {
95819
- setReplCommandNames: () => setReplCommandNames,
95820
- isReplCommandKnown: () => isReplCommandKnown,
95821
- getReplCommandNames: () => getReplCommandNames,
95822
- REPL_DENYLIST: () => REPL_DENYLIST
95823
- });
95824
- function setReplCommandNames(input) {
95825
- const allowed = input.filter((n) => !REPL_DENYLIST.has(n));
95826
- names = allowed;
95827
- known = new Set(allowed);
95828
- }
95829
- function getReplCommandNames() {
95830
- return names;
95831
- }
95832
- function isReplCommandKnown(name2) {
95833
- return known.has(name2);
95834
- }
95835
- var REPL_DENYLIST, names, known;
95836
- var init_command_registry = __esm(() => {
95837
- REPL_DENYLIST = new Set(["shell"]);
95838
- names = [];
95839
- known = new Set;
95840
- });
95841
-
95842
96313
  // src/core/repl/command-dispatcher.ts
95843
96314
  function parseCommandLine(input) {
95844
96315
  const trimmed = input.trim();
@@ -95874,12 +96345,9 @@ function splitRespectingQuotes(input) {
95874
96345
  }
95875
96346
  return tokens;
95876
96347
  }
95877
- function isKnownCommand(name2) {
95878
- return isReplCommandKnown(name2);
96348
+ function isKnownCommand(name2, commandNames) {
96349
+ return commandNames.includes(name2);
95879
96350
  }
95880
- var init_command_dispatcher = __esm(() => {
95881
- init_command_registry();
95882
- });
95883
96351
 
95884
96352
  // src/core/repl/history-manager.ts
95885
96353
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -96006,7 +96474,7 @@ class ReplEngine {
96006
96474
  }
96007
96475
  async handleCommand(input) {
96008
96476
  const parsed = parseCommandLine(input);
96009
- if (!isKnownCommand(parsed.command)) {
96477
+ if (!isKnownCommand(parsed.command, this.context.commandNames)) {
96010
96478
  if (this.context.system === "redis") {
96011
96479
  this.history.add(input);
96012
96480
  return this.executeSql(input);
@@ -96149,7 +96617,6 @@ var import_picocolors3;
96149
96617
  var init_repl_engine = __esm(() => {
96150
96618
  init_input_classifier();
96151
96619
  init_meta_commands();
96152
- init_command_dispatcher();
96153
96620
  init_history_manager();
96154
96621
  init_permission_guard();
96155
96622
  init_query_result_formatter();
@@ -96197,7 +96664,7 @@ function createCompleter(ctx) {
96197
96664
  }
96198
96665
  const words = trimmed.split(/\s+/);
96199
96666
  if (words.length === 1) {
96200
- const cmdHits = matchWithSuffix([...getReplCommandNames()], lastWord);
96667
+ const cmdHits = matchWithSuffix([...ctx.commandNames], lastWord);
96201
96668
  const sqlHits = matchWithSuffix([...SQL_KEYWORDS_FOR_COMPLETION], lastWord);
96202
96669
  return [[...cmdHits[0], ...sqlHits[0]], lastWord];
96203
96670
  }
@@ -96250,7 +96717,6 @@ function matchWithSuffix(candidates, prefix) {
96250
96717
  var REDIS_COMMANDS, TABLE_POSITION_KEYWORDS, COMMANDS_TAKING_TABLE_ARG;
96251
96718
  var init_completer = __esm(() => {
96252
96719
  init_types10();
96253
- init_command_registry();
96254
96720
  init_command_metadata();
96255
96721
  REDIS_COMMANDS = Object.keys(REDIS_COMMAND_TABLE);
96256
96722
  TABLE_POSITION_KEYWORDS = new Set([
@@ -96439,6 +96905,20 @@ var init_es_shell = __esm(() => {
96439
96905
  import_picocolors4 = __toESM(require_picocolors(), 1);
96440
96906
  });
96441
96907
 
96908
+ // src/core/repl/command-registry.ts
96909
+ var exports_command_registry = {};
96910
+ __export(exports_command_registry, {
96911
+ deriveReplCommandNames: () => deriveReplCommandNames,
96912
+ REPL_DENYLIST: () => REPL_DENYLIST
96913
+ });
96914
+ function deriveReplCommandNames(topLevel) {
96915
+ return topLevel.filter((n) => !REPL_DENYLIST.has(n));
96916
+ }
96917
+ var REPL_DENYLIST;
96918
+ var init_command_registry = __esm(() => {
96919
+ REPL_DENYLIST = new Set(["shell"]);
96920
+ });
96921
+
96442
96922
  // src/commands/shell.ts
96443
96923
  import { createInterface as createInterface3 } from "readline";
96444
96924
  import { join as join32 } from "path";
@@ -96463,6 +96943,14 @@ async function populateMongoColumns(mongoAdapter, collectionNames, threshold = M
96463
96943
  }
96464
96944
  return columnsByTable;
96465
96945
  }
96946
+ async function populateRedisKeyCompletion(adapter, limit = REDIS_COMPLETION_KEY_LIMIT) {
96947
+ try {
96948
+ const { names, truncated } = await adapter.sampleKeyNames(limit);
96949
+ return { tableNames: names, truncated };
96950
+ } catch {
96951
+ return { tableNames: [], truncated: false };
96952
+ }
96953
+ }
96466
96954
  async function runShell(options, configPath) {
96467
96955
  let config;
96468
96956
  try {
@@ -96490,9 +96978,12 @@ async function runShell(options, configPath) {
96490
96978
  let tableNames = [];
96491
96979
  let columnsByTable = {};
96492
96980
  if (isRedis) {
96493
- const keys = await adapter.listTables();
96494
- tableNames = keys.map((k) => k.name);
96981
+ const completion = await populateRedisKeyCompletion(redisInner);
96982
+ tableNames = completion.tableNames;
96495
96983
  columnsByTable = {};
96984
+ if (completion.truncated) {
96985
+ console.error(import_picocolors5.default.dim(`Redis shell: large keyspace; tab completion limited to the first ${REDIS_COMPLETION_KEY_LIMIT} keys.`));
96986
+ }
96496
96987
  } else if (isMongoDB) {
96497
96988
  const collections = await adapter.listTables();
96498
96989
  tableNames = collections.map((collection) => collection.name);
@@ -96512,17 +97003,18 @@ async function runShell(options, configPath) {
96512
97003
  }
96513
97004
  }
96514
97005
  }
97006
+ const { buildProgram } = await Promise.resolve().then(() => (init_program(), exports_program));
97007
+ const { buildCompletionTree: buildCompletionTree2, listTopLevelCommandNames: listTopLevelCommandNames2 } = await Promise.resolve().then(() => exports_command_tree);
97008
+ const { deriveReplCommandNames: deriveReplCommandNames2 } = await Promise.resolve().then(() => (init_command_registry(), exports_command_registry));
97009
+ const commandNames = deriveReplCommandNames2(listTopLevelCommandNames2(buildCompletionTree2(buildProgram())));
96515
97010
  const context = {
96516
97011
  configPath,
96517
97012
  permission: config.permission,
96518
97013
  system: config.connection.system,
96519
97014
  tableNames,
96520
- columnsByTable
97015
+ columnsByTable,
97016
+ commandNames
96521
97017
  };
96522
- const { buildProgram } = await Promise.resolve().then(() => (init_program(), exports_program));
96523
- const { buildCompletionTree: buildCompletionTree2, listTopLevelCommandNames: listTopLevelCommandNames2 } = await Promise.resolve().then(() => exports_command_tree);
96524
- const { setReplCommandNames: setReplCommandNames2 } = await Promise.resolve().then(() => (init_command_registry(), exports_command_registry));
96525
- setReplCommandNames2(listTopLevelCommandNames2(buildCompletionTree2(buildProgram())));
96526
97018
  const engine = new ReplEngine(adapter, context, HISTORY_PATH, config);
96527
97019
  const complete = createCompleter(context);
96528
97020
  console.error(import_picocolors5.default.bold(t_vars("shell.welcome", {
@@ -96612,7 +97104,7 @@ async function runBatchSession(engine, input) {
96612
97104
  }
96613
97105
  }
96614
97106
  }
96615
- var import_picocolors5, HISTORY_PATH, MONGO_COMPLETION_EAGER_THRESHOLD = 20, shellCommand;
97107
+ var import_picocolors5, HISTORY_PATH, MONGO_COMPLETION_EAGER_THRESHOLD = 20, REDIS_COMPLETION_KEY_LIMIT = 1000, shellCommand;
96616
97108
  var init_shell3 = __esm(() => {
96617
97109
  init_esm();
96618
97110
  init_config();
@@ -96730,30 +97222,30 @@ class PostgreSQLDDLGenerator {
96730
97222
  dropIndex(indexName, _table) {
96731
97223
  return { sql: `DROP INDEX ${q(indexName)};`, warnings: [] };
96732
97224
  }
96733
- addConstraint(constraint) {
96734
- const t2 = q(constraint.table);
97225
+ addConstraint(constraint2) {
97226
+ const t2 = q(constraint2.table);
96735
97227
  const warnings = [];
96736
- switch (constraint.type) {
97228
+ switch (constraint2.type) {
96737
97229
  case "foreign_key": {
96738
- const name2 = constraint.name || `fk_${constraint.table}_${constraint.column}`;
96739
- const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
97230
+ const name2 = constraint2.name || `fk_${constraint2.table}_${constraint2.column}`;
97231
+ const onDelete = constraint2.onDelete ? ` ON DELETE ${constraint2.onDelete.toUpperCase().replace("_", " ")}` : "";
96740
97232
  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};`,
97233
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} FOREIGN KEY (${q(constraint2.column)}) REFERENCES ${q(constraint2.references.table)}(${q(constraint2.references.column)})${onDelete};`,
96742
97234
  warnings
96743
97235
  };
96744
97236
  }
96745
97237
  case "unique": {
96746
- const name2 = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
96747
- const cols = constraint.columns.map(q).join(", ");
97238
+ const name2 = constraint2.name || `uq_${constraint2.table}_${constraint2.columns.join("_")}`;
97239
+ const cols = constraint2.columns.map(q).join(", ");
96748
97240
  return {
96749
97241
  sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} UNIQUE (${cols});`,
96750
97242
  warnings
96751
97243
  };
96752
97244
  }
96753
97245
  case "check": {
96754
- const name2 = constraint.name || `ck_${constraint.table}`;
97246
+ const name2 = constraint2.name || `ck_${constraint2.table}`;
96755
97247
  return {
96756
- sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} CHECK (${constraint.expression});`,
97248
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q(name2)} CHECK (${constraint2.expression});`,
96757
97249
  warnings
96758
97250
  };
96759
97251
  }
@@ -96903,30 +97395,30 @@ class MySQLDDLGenerator {
96903
97395
  warnings: table ? [] : ["MySQL requires ON <table> \u2014 specify table for this operation"]
96904
97396
  };
96905
97397
  }
96906
- addConstraint(constraint) {
96907
- const t2 = q2(constraint.table);
97398
+ addConstraint(constraint2) {
97399
+ const t2 = q2(constraint2.table);
96908
97400
  const warnings = [];
96909
- switch (constraint.type) {
97401
+ switch (constraint2.type) {
96910
97402
  case "foreign_key": {
96911
- const name2 = constraint.name || `fk_${constraint.table}_${constraint.column}`;
96912
- const onDelete = constraint.onDelete ? ` ON DELETE ${constraint.onDelete.toUpperCase().replace("_", " ")}` : "";
97403
+ const name2 = constraint2.name || `fk_${constraint2.table}_${constraint2.column}`;
97404
+ const onDelete = constraint2.onDelete ? ` ON DELETE ${constraint2.onDelete.toUpperCase().replace("_", " ")}` : "";
96913
97405
  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};`,
97406
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} FOREIGN KEY (${q2(constraint2.column)}) REFERENCES ${q2(constraint2.references.table)}(${q2(constraint2.references.column)})${onDelete};`,
96915
97407
  warnings
96916
97408
  };
96917
97409
  }
96918
97410
  case "unique": {
96919
- const name2 = constraint.name || `uq_${constraint.table}_${constraint.columns.join("_")}`;
96920
- const cols = constraint.columns.map(q2).join(", ");
97411
+ const name2 = constraint2.name || `uq_${constraint2.table}_${constraint2.columns.join("_")}`;
97412
+ const cols = constraint2.columns.map(q2).join(", ");
96921
97413
  return {
96922
97414
  sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} UNIQUE (${cols});`,
96923
97415
  warnings
96924
97416
  };
96925
97417
  }
96926
97418
  case "check": {
96927
- const name2 = constraint.name || `ck_${constraint.table}`;
97419
+ const name2 = constraint2.name || `ck_${constraint2.table}`;
96928
97420
  return {
96929
- sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} CHECK (${constraint.expression});`,
97421
+ sql: `ALTER TABLE ${t2} ADD CONSTRAINT ${q2(name2)} CHECK (${constraint2.expression});`,
96930
97422
  warnings: ["CHECK constraints enforced in MySQL 8.0.16+ and MariaDB 10.2.1+ only"]
96931
97423
  };
96932
97424
  }