Skip to content

Latest commit

 

History

History
1761 lines (1330 loc) · 78.9 KB

File metadata and controls

1761 lines (1330 loc) · 78.9 KB

CLI Reference

deltascope is the primary operator surface for local audits, CI pipeline checks, and agent workflows. It provides commands for auditing SQL, inspecting rules, managing policy configuration, and querying engine capabilities.


Global Flags

These flags apply to all subcommands.

Flag Type Default Description
--config string (none) Path to YAML policy config file. When omitted, policy.Default() is used.
--dialect string mysql SQL dialect: mysql, tidb, or postgresql. PostgreSQL requires a PG-capable DeltaScope binary. Starting with the v0.17.0 public release line, the supported macOS and Linux deltascope archives are PG-capable, so PostgreSQL offline audit uses the normal main CLI path. In metadata-aware mode, dialect is auto-detected from the live MySQL/TiDB-compatible instance; an explicit --dialect that conflicts with the detected dialect causes exit 2.
--quiet bool false Suppress non-result output. With markdown output, each finding is printed as a single line; JSON output is unchanged.
--version bool false Print the build version and compiled dialects, then exit. Release archives print the release tag. Source and go install @main builds print Go module or VCS information.

Cobra also exposes built-in -h / --help flags on every command. Host uses --host and -H; bare -h prints help and exits 0.


deltascope audit

Audit one or more SQL statements from inline text, a file, or standard input.

Input

The three input sources are mutually exclusive. If --sql and --file are both omitted, deltascope audit reads from stdin, making it easy to pipe SQL through the tool. An explicit --sql value, including "" or whitespace-only text, is the SQL input and is rejected with audit: SQL input must not be empty (exit 2) instead of falling through to stdin.

Flag Description
--sql Inline SQL text to audit. Value: <text>
--file Path to a .sql file to audit. Value: <path>
(none) Read SQL from stdin

Examples:

# Inline SQL
deltascope audit --sql "DELETE FROM users"

# From a file
deltascope audit --file ./migrations/v2.sql

# From stdin
cat migrations/v2.sql | deltascope audit

# With a non-default policy and JSON output
deltascope audit --config ./deltascope.yaml --format json --file ./migrations/v2.sql

Output and Exit Flags

These flags belong to audit only. Their canonical placement is after audit; legacy forms placed before audit remain supported for compatibility. Query Access always emits its fixed JSON document; it does not accept audit rendering or finding-threshold options. Passing --format or --fail-on to Query Access, before or after the command, returns a usage error (exit 3) without an analysis document.

Flag Type Default Description
--format string markdown Output format: markdown (human-readable local report), json (stable machine-readable contract), github-actions (inline GitHub Actions annotations), github-summary (Markdown for $GITHUB_STEP_SUMMARY), sarif (SARIF 2.1.0 for GitHub Code Scanning and SARIF consumers), or gitlab-codequality (GitLab Code Quality artifact).
--include-skipped-rules bool false JSON only: add the complete per-rule skipped list as rule_summary.skipped_rules; the aggregate rule_summary.skipped field remains unchanged. Other formats are unchanged.
--fail-on string blocker Fail Threshold for process exit: blocker, warning, notice, or none. Controls exit code only; it does not change Verdict.

Connection Flags (Metadata-Aware Mode)

Metadata-aware mode is activated by an endpoint, credential, or schema flag: --host, --port, --user, --password-env, --password-file, --ask-password, --schema, or --socket. DeltaScope then connects to the specified instance and retrieves live schema facts before rule evaluation. --database, --tls-mode, --tls-ca-file, and --metadata-connect-timeout configure that connection but do not activate metadata-aware mode on their own.

Flag Short Default Description
--host -H (none) Database host address
--port -P 3306 Port number. With --port omitted, defaults to 5432 only when --dialect postgresql is explicit; otherwise defaults to 3306. An explicit port always wins.
--user -u (none) Database user
--password-env (none) Environment variable that contains the database password
--password-file (none) File path that contains the database password
--ask-password false Prompt for password interactively. Mutually exclusive with --password-env and --password-file.
--database (none) Database/catalog name. For MySQL/TiDB it aliases --schema; for PostgreSQL it selects the database (required when --schema is set)
--schema -D (none) Default schema for unqualified table name resolution. For MySQL/TiDB it selects the catalog and aliases --database
--socket -S (none) Unix socket path. Mutually exclusive with --host/--port and --tls-mode enabled.
--tls-mode disabled TLS connection mode: disabled or enabled. When enabled, requires --host and --user; rejects --socket.
--tls-ca-file (none) Path to a CA certificate file for TLS verification. Only used when --tls-mode enabled.
--metadata-connect-timeout (none) Metadata connection timeout for metadata-aware audit, for example 5s or 500ms

Migration note: The --password / -p flag has been removed. Use --password-env, --password-file, or --ask-password instead. Scripts that passed --password on the command line should switch to one of these secure password sources.

Behavior in metadata-aware mode:

  • For MySQL/TiDB: dialect is auto-detected from the live instance by querying tidb_version(). If --dialect is also set explicitly and conflicts, the command exits with code 2. --database selects the catalog and is an alias for --schema; either flag alone works, matching values are accepted, and conflicting values fail before an explicitly selected MySQL/TiDB metadata connection opens. Auto-detected connections apply the same check after identity detection so PostgreSQL database/schema values remain distinct. When --port is omitted, this path keeps the MySQL-oriented default of 3306.
  • For PostgreSQL: pass --dialect postgresql explicitly and use --database to select the database (postgres when omitted). --schema selects the schema within that database; when --schema is explicitly set, --database is required and is never inferred from the schema value. Omitting both preserves default catalog resolution. When --port is omitted, the explicit PostgreSQL selection uses 5432; an explicit port always wins. The CLI does not probe services to infer a port.
  • Schema resolution order for unqualified table names: SQL-level qualifier → --schema flag → unique match across accessible schemas → error if ambiguous.
  • Connection failures print one bounded stderr line. Portable output never includes host, port, user, DSN, password, or raw driver text. Empty passwords remain allowed; a missing --password-env / --password-file / --ask-password is reported only after the server rejects that empty password.
Situation stderr Exit
Missing or unreadable --password-env / --password-file invalid password source 2
Authentication failed and no password source was set password source required: use --password-env, --password-file, or --ask-password 2
Authentication failed after a password source was set authentication failed 3
Typed connection refusal connection refused 3
Other server unreachable or dial failure connection failed 3
Connect timeout connection timed out 3
TLS certificate hostname mismatch TLS hostname mismatch 3
TLS certificate authority is unknown or untrusted TLS unknown certificate authority 3
TLS server did not offer TLS TLS server did not offer TLS 3
Other TLS certificate verification failure TLS certificate verification failed 3
Other TLS handshake failure TLS handshake failed 3

TLS failure messages are bounded categories. They never include the target host or address, certificate identity, credentials, DSN, CA-file path, or raw driver text. Hostname verification remains enabled. Stock MySQL 8.4 auto-generated server certificates have no hostname-valid SAN; supplying the server's ca.pem establishes trust but does not make that certificate valid for localhost or an IP address. Use a server certificate with a DNS/IP SAN matching --host and the corresponding trusted CA.

Non-TLS protocol or handshake failures remain connection failed because the supported drivers do not expose a stable cross-driver typed signal for that category. The CLI does not infer it from raw driver text.

Examples:

# Connect to a local MySQL/TiDB instance (dialect auto-detected; --database selects the catalog)
deltascope audit \
  --host 127.0.0.1 --port 3306 \
  --user dba --ask-password \
  --database mydb \
  --file ./migration.sql

# Use a Unix socket
deltascope audit \
  --socket /var/run/mysqld/mysqld.sock \
  --user dba --password-env DELTASCOPE_DB_PASSWORD \
  --database mydb \
  --sql "ALTER TABLE orders ADD COLUMN status TINYINT NOT NULL DEFAULT 0"

# Connect to a PostgreSQL instance
deltascope audit \
  --host 127.0.0.1 --port 5432 \
  --user readonly --ask-password \
  --dialect postgresql --database app --schema public \
  --file ./migration.sql

# Connect to MySQL over TLS
deltascope audit \
  --host db.example.com --port 3306 \
  --user dba --ask-password \
  --tls-mode enabled \
  --database mydb \
  --file ./migration.sql

# Connect to PostgreSQL over TLS with a custom CA certificate
deltascope audit \
  --host pg.example.com --port 5432 \
  --user readonly --ask-password \
  --tls-mode enabled --tls-ca-file /etc/ssl/certs/pg-ca.pem \
  --dialect postgresql --database app --schema public \
  --file ./migration.sql

Output Formats

Markdown Output (default)

Human-readable output, suitable for review in terminals and pull request comments. Statement headings are 1-based in markdown, even though the JSON index field is 0-based.

# DeltaScope Audit Result

Verdict: `reject`

- Statements: 1
- Blockers: 1
- Warnings: 0
- Notices: 0

## Action Summary

- [blocker] `dml.where.require`: 1 finding
  Summary: Require DML where require
  Suggestion: Add the required clause, option, or object explicitly so the rule no longer has to infer intent.
  Explain: deltascope rules explain dml.where.require
  Statements: 1

## Result Explanation

Audit produced 1 finding(s) across 1 statement(s)
- UPDATE and DELETE statements must include a WHERE clause

## Statement 1

- Kind: `dml`
- SQL: `delete from users`

### Explanation

Statement 1 has 1 finding(s)
- UPDATE and DELETE statements must include a WHERE clause

### Findings

- [blocker] `dml.where.require`: UPDATE and DELETE statements must include a WHERE clause
  Why: The statement is missing a clause, option, or object that the shipped policy requires.
  Risk: Ignoring this rule can allow high-impact data changes to proceed with less safety review.
  Suggestion: add a WHERE clause that narrows the affected rows
  Statement kind: `dml`
  Metadata:
  - `operation`: `delete`

Action Summary

When a markdown audit has findings, both the default deltascope audit path and --format markdown render an ## Action Summary section between the counts (Statements / Blockers / Warnings / Notices) and ## Result Explanation. It groups findings by rule_id so you can see what to fix first without reading the report statement-by-statement.

Each rule group shows, at most:

  • [level] \rule_id`: N finding(s)— the prioritylevel (blocker, warning, or notice`) and the deduplicated finding count
  • Summary: and Suggestion: — rule catalog text when available, otherwise the first finding's message and suggestion
  • Explain: deltascope rules explain <rule_id> — a copy-paste command to inspect the rule
  • Statements: — 1-based indexes of the statements that triggered the rule (deduplicated; omitted for global-only findings)
  • Scope: global — present only when the group includes a global finding

Groups are ordered by remediation priority: blocker, then warning, then notice, then by finding count descending, then by rule_id ascending. At most 10 rule groups are shown; when there are more, a final Showing 10 of N rule groups. line is appended.

Clean audits (no findings) omit the section entirely. The summary carries no raw SQL and no finding metadata — only rule IDs, levels, counts, catalog text, 1-based statement indexes, and the explain command. Offline audits prepend one limitation line when the section is present: existence not checked (no database connection).

Scope and non-goals:

  • The Action Summary is markdown-only. Audit JSON output does not add an action_summary field, and the finding JSON shape is unchanged.
  • level remains the priority field; no severity field is introduced.
  • SDK, HTTP, MCP, SARIF, GitHub Actions, and GitLab Code Quality outputs are unchanged.
  • This adds no parser support, no audit rules, and no change to audit or rule behavior.
  • The text layout is a human-readable aid, not a machine contract. For automation, use --format json and aggregate findings yourself.

JSON Output

Machine-readable output with a stable schema. Use --format json in CI pipelines and tooling integrations.

deltascope audit --format json --sql "DELETE FROM users"

CLI JSON always includes a top-level context object. In offline mode it reports the configured dialect source plus note / unproven when existence was not checked; in metadata-aware mode it also reports resolved schema details. Completed CLI JSON also includes fail_on_triggered beside the audit Result: true when Fail Threshold caused a non-zero process exit, false otherwise. Fail Threshold does not change Verdict (pass / review / reject from blockers and warnings; notices do not change Verdict). SDK, HTTP, and MCP Result do not include this field.

{
  "verdict": "reject",
  "summary": {
    "statements": 1,
    "blockers": 1,
    "warnings": 0,
    "notices": 0
  },
  "explanation": {
    "summary": "Audit produced 1 finding(s) across 1 statement(s)",
    "reasons": [
      "UPDATE and DELETE statements must include a WHERE clause"
    ]
  },
  "statements": [
    {
      "index": 0,
      "kind": "dml",
      "raw_sql": "delete from users",
      "normalized_sql": "delete from users",
      "explanation": {
        "summary": "Statement 1 has 1 finding(s)",
        "reasons": [
          "UPDATE and DELETE statements must include a WHERE clause"
        ]
      },
      "findings": [
        {
          "rule_id": "dml.where.require",
          "level": "blocker",
          "message": "UPDATE and DELETE statements must include a WHERE clause",
          "statement_kind": "dml",
          "suggestion": "add a WHERE clause that narrows the affected rows",
          "explanation": {
            "summary": "Require DML where require",
            "why": "The statement is missing a clause, option, or object that the shipped policy requires.",
            "risk": "Ignoring this rule can allow high-impact data changes to proceed with less safety review.",
            "suggestion": "add a WHERE clause that narrows the affected rows"
          }
        }
      ]
    }
  ],
  "fail_on_triggered": true,
  "context": {
    "mode": "offline",
    "dialect": "mysql",
    "dialect_source": "default",
    "note": "existence not checked (no database connection)",
    "unproven": ["column_exists", "table_exists"]
  }
}

DML Impact Estimation

When DeltaScope audits UPDATE or DELETE, it may add an impact object to each statement result. The object is conservative by design and reports estimated_rows, estimated_ratio, risk_level, confidence, source, reason_codes, and optional notes.

{
  "raw_sql": "DELETE FROM users WHERE id = 42",
  "impact": {
    "estimated_rows": 1,
    "estimated_ratio": 0.0001,
    "risk_level": "low",
    "confidence": "high",
    "source": "metadata",
    "reason_codes": ["pk_equality"],
    "notes": ["refined with table statistics"]
  }
}

Offline mode uses SQL shape only. MySQL, TiDB, and PostgreSQL share the bounded single-table id = literal/parameter equality heuristic; PostgreSQL $1 placeholders are supported. Non-equality, OR, range, and unrecognized-column predicates remain unknown, while a missing WHERE remains a full-table estimate. Metadata-aware mode may refine the estimate with read-only table statistics (MySQL/TiDB) or via the PostgreSQL query planner (EXPLAIN). A live PostgreSQL estimate remains source: plan and overrides the shape estimate. DeltaScope does not execute the DML and does not run EXPLAIN ANALYZE.

Threshold rules dml.impact.rows.max_count and dml.impact.ratio.max_percent are opt-in: they are cataloged as default-disabled and consume this additive statement-level payload only when caller config enables them. The payload itself is attached in the audit flow before rule evaluation, including default audits.

Metadata-Aware JSON Output

When metadata-aware mode is active, the JSON response includes an additional context field describing how dialect and schema were resolved.

{
  "verdict": "pass",
  "summary": { "statements": 1, "blockers": 0, "warnings": 0, "notices": 0 },
  "statements": [...],
  "context": {
    "mode": "metadata-aware",
    "dialect": "mysql",
    "dialect_source": "detected",
    "schema": "app",
    "schema_source": "inferred"
  }
}

dialect_source values: "default" (offline default), "flag" (from --dialect), or "detected" (from a live instance in metadata-aware mode). For CLI metadata-aware audits, schema_source values are "database" (the MySQL/TiDB --database alias), "flag" (from --schema), or "inferred" (unique match across accessible schemas). When schema inference is unnecessary or unavailable, the field may be omitted instead of emitting an extra source value.

Quiet Mode

--quiet changes markdown output only. With markdown output, DeltaScope suppresses the normal report body and prints each finding as a single line. With --format json, the JSON contract is unchanged.

[blocker] dml.where.require: UPDATE and DELETE statements must include a WHERE clause

This is useful for scripted processing or minimalist CI log output.

GitHub Actions Output

Use --format github-actions to produce inline CI annotations that render in the GitHub Actions workflow log.

deltascope audit --dialect postgresql --file ./migrations/20260409_add_index.sql --format github-actions

Each finding maps to a GitHub Actions workflow command (::error, ::warning, or ::notice) based on the rule level (blocker::error, warning::warning, notice::notice). Special characters in titles and messages are escaped per the GitHub workflow command specification. When --file is provided, each annotation includes file=<path>,line=N,col=N pointing at the exact statement that triggered the finding.

Each finding annotation is self-contained:

  • The title is [<level>] <rule_id>, for example [blocker] dml.where.require.
  • The message keeps the finding message, adds an optional Suggestion: line, and appends a trailing Explain: deltascope rules explain <rule_id> line, so a reviewer can copy-paste the explain command without opening the full report.
  • Unsupported-statement notices carry no Explain: line, because unsupported statements have no rule id.

GitHub Job Summary Output

Use --format github-summary when writing a short review summary to $GITHUB_STEP_SUMMARY in GitHub Actions:

deltascope audit --file ./migrations.sql --format github-summary --fail-on none >> "$GITHUB_STEP_SUMMARY"

This output is GitHub-flavored Markdown for humans. It includes the verdict, counts, and Action Summary, and it omits raw SQL. It is not a machine-readable contract. Use --format json, --format sarif, or --format gitlab-codequality for automation.

SARIF Output

Use --format sarif to produce valid SARIF 2.1.0 JSON for GitHub Code Scanning, Azure DevOps, and other SARIF consumers.

deltascope audit --file ./migrations.sql --dialect postgresql --format sarif > deltascope.sarif

The output includes rule metadata (help text from explanation suggestions) under tool.driver.rules and maps severity levels to SARIF levels: blockererror, warningwarning, noticenote. When --file is provided, each result includes artifactLocation.uri, startLine, and startColumn pointing at the exact statement.

GitLab Code Quality Output

Use --format gitlab-codequality to produce a GitLab Code Quality report for merge request Code Quality widgets and diff annotations. Available in all GitLab tiers (Free+).

deltascope audit --file migrations.sql --format gitlab-codequality --fail-on none > gl-code-quality-report.json

In .gitlab-ci.yml, publish the report as an artifact:

artifacts:
  reports:
    codequality: gl-code-quality-report.json
  when: always

Field mapping:

DeltaScope GitLab Code Quality
Rule ID check_name
Message + suggestion description
blocker → major, warning → minor, notice → info severity
--file path or deltascope.sql location.path
Finding line or 1 location.lines.begin
SHA-256 hash fingerprint

Fingerprints are stable across runs so GitLab can track findings across pipelines. Unsupported statements (parser diagnostics) are not emitted as Code Quality issues. location.lines.begin carries the statement-start line number from the source mapper. See use-deltascope-in-gitlab-ci.md for a complete recipe.

Rule Summary

JSON, markdown, and quiet output include a rule summary showing how many rules were Loaded for this audit, how many were applicable, and how many were skipped with a known reason. These are different facts from Rule Catalog and Default Policy:

  • Rule Catalogdeltascope rules list / list_rules. Includes default-disabled catalog-only rules such as dml.impact.*.
  • Default Policy — the enabled-rule set used when the caller supplies no config. It does not enable dml.impact.*.
  • Loadedrule_summary.loaded. Statement rules actually registered for this audit. It is not the Catalog size and not the Default Policy key count. Catalog-only opt-in rules are not Loaded unless enabled. The three ddl.constraint.foreign_key.name.* rules stay in Default Policy but are not Loaded while ddl.table.foreign_key.forbid is enabled; config status reports that as reason fk_forbid.
  • Skipped — Loaded rules that did not apply, with a known reason such as dialect_mismatch. FK-forbid suppression is not a per-statement skip.

In CLI JSON this appears as rule_summary; rule_summary.skipped is always a deterministic, reason-sorted array of { "reason": "...", "count": N } objects, with counts covering every skipped rule (and [] when none were skipped). It never contains rule_id. Pass --include-skipped-rules to add the stable full per-rule list in a separate rule_summary.skipped_rules field; that optional field is omitted by default, and the aggregate skipped field remains present. Example counts below show shape only; they are not a frozen Catalog or Loaded size:

{
  "rule_summary": {
    "loaded": 12,
    "applicable": 4,
    "skipped": [
      {"reason": "dialect_mismatch", "count": 8}
    ],
    "skipped_rules": [
      {"rule_id": "ddl.pg.alter.add_check.not_valid.require", "reason": "dialect_mismatch"}
    ]
  }
}

Use deltascope audit --format json --include-skipped-rules ... when exact skipped rule IDs are needed. In markdown it renders as ## Rule Summary with Loaded, Applicable, Skipped with known reason, and — when any skip reason is recorded — a ### Skip Reasons subsection aggregating the skipped rules by reason code, ordered deterministically. Markdown never expands skipped rule IDs. GitHub Actions and SARIF output do not include rule summary.

PostgreSQL Trust Signals

When auditing on the MySQL/TiDB path, DeltaScope may detect PostgreSQL-specific syntax and emit a dialect.postgresql.syntax.detected.notice global finding. This is an advisory notice — DeltaScope does not auto-switch dialect.

In markdown output, a ## Audit Context section appears with an explicit trust note when this notice fires:

## Audit Context
- Mode: `offline`
- Dialect: `mysql` (default)
- Trust Note: Dialect remains `mysql` (default). DeltaScope did not auto-switch dialect.

In JSON output, the top-level context object always reports mode, dialect, and dialect_source. Offline audits also report that existence was not checked:

{
  "context": {
    "mode": "offline",
    "dialect": "mysql",
    "dialect_source": "default",
    "note": "existence not checked (no database connection)",
    "unproven": ["column_exists", "table_exists"]
  }
}

--quiet does not change the JSON contract. Findings stay on statements[].findings; there is no top-level findings array. Agents that combine --quiet --format json should read context.note / context.unproven rather than inferring safety from mode or verdict alone.

In quiet output, a [context] line is appended. Offline audits append the same existence caveat:

[context] mode=offline dialect=mysql dialect_source=default existence not checked (no database connection)

In markdown, offline audits also put that line in ## Audit Context and, when findings exist, at the top of ## Action Summary. Mode: offline is a label; the existence line is the limitation. Offline ALTER still passes when only policy notices ran — the caveat means existence blockers such as ddl.alter.drop_column.exists.require did not run.

If the SQL does target PostgreSQL, re-run with --dialect postgresql. If not, the notice can be safely ignored.

MySQL DML RETURNING Dialect Notice

The TiDB parser recognizes DML RETURNING for INSERT, UPDATE, and single-table DELETE. TiDB supports this syntax; MySQL Server does not. On the MySQL dialect, a parsed RETURNING clause emits a dialect.mysql.returning.unsupported.notice global finding so the unsupported boundary is visible instead of silently accepted. RETURNING is no longer treated as a PostgreSQL-only token, so TiDB RETURNING no longer triggers dialect.postgresql.syntax.detected.notice.

If the SQL targets TiDB, re-run with --dialect tidb. REPLACE ... RETURNING is not supported and keeps its parser-error/unsupported path.

PostgreSQL Capability-Boundary Errors

When a PG-capable DeltaScope binary encounters PostgreSQL-specific functionality that is not yet fully supported (e.g., DDL parsing), it returns a typed PostgreSQLCapabilityBoundaryError. This distinguishes known capability limits from real parse failures. The error includes a clear message about what surface was requested and what the current build supports.

Parser-Error Unsupported Contract

When the selected dialect parser cannot parse a tracked DDL statement, DeltaScope returns a diagnostic stating that no audit was performed and no findings were inferred from the unparsed SQL. This is an unsupported parser surface, not a fallback parser. DeltaScope does not infer findings from unparsed SQL. The parser-error count is not reduced by this contract. No parser support is added, no fallback parser is introduced, and no new SQL audit rules are created. The diagnostic message is: statement was not audited because the selected dialect parser could not parse it; no audit findings were inferred. On the CLI this path exits 2 (bad user input), including when valid sibling statements were audited. summary describes only statements that reached normal rule evaluation. When mixed input includes any audited=false parser_error diagnostic, JSON and Markdown verdict is at least review if aggregation would otherwise have produced pass; existing review and reject verdicts do not downgrade. A wholly unparsable input still leaves verdict empty. Consumers must also inspect diagnostics[].classification == parser_error. This is not a new error or unsupported verdict. Mixed input retains audited siblings in original source order with their findings, impact estimates, and source locations.

Unsupported Diagnostics Evidence (v0.230.0)

Starting with v0.230.0, parser-error and unsupported statement outcomes expose structured diagnostic evidence through all public surfaces (CLI JSON/text, HTTP, MCP, SDK). Each diagnostic carries these fields:

Field Type Meaning
classification string Stable category: parser_error or unsupported_statement
reason string Safe human-readable explanation of why the statement was not audited
action_hint string Generic next step for the user
audited bool false — the statement was not audited
dialect string Selected dialect when available
guidance_code string Optional machine-readable boundary category (v0.260.0+)
evidence_ref string Optional GitHub documentation URL for the boundary evidence (v0.260.0+)
line int Optional 1-based start line of a bounded parser-failed statement
column int Optional 1-based start column of a bounded parser-failed statement

For parser_error diagnostics, reason contains the v0.220.0 standard diagnostic message and action_hint suggests verifying the dialect and syntax, splitting multi-statement input, or upgrading DeltaScope.

For unsupported_statement diagnostics, reason reuses the existing UnsupportedDetail.Reason and action_hint suggests manual review.

Diagnostics do not contain raw SQL text, parser near ... fragments, routine bodies, or any other forbidden payload. This is not parser support, not a fallback parser, and not new SQL audit rules. Parser-error counts are not reduced. The census is unchanged.

Parser Upgrade Candidate Evidence (v0.250.0)

Starting with v0.250.0, all 29 remaining parser-error DDL cases (MySQL 15, TiDB 9, PostgreSQL 5) are classified by feasibility bucket. This classification is a documented evidence pack — not current parser support, not a fallback parser, and not new SQL audit rules.

Feasibility bucket facts:

Bucket MySQL TiDB PostgreSQL Total
parser_upgrade_candidate 5 0 5 10
bounded_fallback_candidate 1 3 0 4
product_unsupported_or_inapplicable 0 6 0 6
unsafe_fallback_defer 9 0 0 9
needs_research 0 0 0 0

Key points:

  • parser_upgrade_candidate identifies 10 DDL forms (MySQL 5, PostgreSQL 5) that would become parseable after a parser/library upgrade. This is not current support.
  • DeltaScope does not infer findings from failed parse text. No fallback parser is used.
  • CLI output shape is unchanged. parser_upgrade_candidate is a documented classification, not a new CLI field.
  • The parser_error diagnostic still means the statement was not audited. Users should not treat parser_error as PASS.
  • Users should review parser-error statements manually.
  • No promise that future versions will support these syntax forms.

Unsupported Diagnostics Guidance Codes (v0.260.0)

Starting with v0.260.0, parser-error diagnostics for parser-upgrade candidates carry two additional fields to explain why the statement was not audited and where to find detailed evidence:

  • guidance_code — a stable machine-readable string identifying the unsupported boundary category. For parser-upgrade candidates, the value is parser_upgrade_candidate.
  • evidence_ref — a GitHub documentation URL pointing to the relevant evidence section. For parser-upgrade candidates, this links to the Parser Upgrade Candidate Evidence (v0.250.0) section above.

These fields are optional. They appear only when the diagnostic matches a known unsupported boundary. When absent, the diagnostic still carries classification, reason, action_hint, audited, and dialect.

All four public surfaces (SDK, CLI JSON, CLI text, HTTP, MCP) expose these fields consistently. CLI text output appends guidance_code= and evidence_ref= key-value pairs to the [diagnostic] line when present.

Example CLI text output for a parser-upgrade candidate:

[diagnostic] classification=parser_error action_hint=verify the selected dialect and syntax... guidance_code=parser_upgrade_candidate evidence_ref=https://github.com/Fanduzi/DeltaScope/blob/main/docs/reference/cli.md#parser-upgrade-candidate-evidence-v02500

Neither field contains raw SQL, parser near-text, object names, function bodies, or any user payload. This is not new parser support, not a fallback parser, and not new SQL audit rules.

The developer/verification entry point make parser-upgrade-candidate-evidence-report delegates to the existing ddl-parser-error-feasibility-report target. It is not a CLI user command.

For a complete DDL coverage catalog across all dialects with per-form classification, see ddl-coverage.md.

PostgreSQL DDL Coverage

Starting with v0.21.0, DeltaScope normalizes common PostgreSQL migration follow-up DDL through the shared audit pipeline. These forms no longer return capability-boundary errors:

PostgreSQL DDL Action Notes
ALTER TABLE ... ALTER COLUMN ... SET DEFAULT set_default Column default assignment during phased rollout
ALTER TABLE ... ALTER COLUMN ... DROP DEFAULT drop_default Column default removal
ALTER TABLE ... ALTER COLUMN ... SET NOT NULL set_not_null Nullability enforcement after backfill
ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL drop_not_null Nullability relaxation
ALTER TABLE ... VALIDATE CONSTRAINT validate_constraint Constraint validation in the recommended NOT VALIDVALIDATE pattern
ALTER TABLE ... DROP CONSTRAINT drop_constraint Constraint removal; primary-key drops reuse ddl.alter.drop_primary_key rules when metadata is available

Starting with v0.23.0, DeltaScope also audits more common PostgreSQL CREATE TABLE constraint shapes through the same shared pipeline:

PostgreSQL CREATE TABLE shape Supported Auditable Rule-mapped Metadata-dependent Notes
Table-level named CHECK ✓ (shared constraint naming when configured) Reuses existing constraint naming governance where applicable
Column-level inline CHECK Supported structure; no dedicated new rule family
Table-level named UNIQUE ✓ (shared constraint naming when configured) Named constraint facts can flow into existing naming governance
Column-level inline UNIQUE ✓ (shared index facts) Contributes index facts to existing shared index rules
Table-level named FOREIGN KEY ✓ (shared constraint naming when configured) Foreign-key naming rules only matter when policy allows foreign keys
Column-level inline REFERENCES Exposed as parser-owned shared facts only; no invented metadata semantics

Examples:

# Create-table coverage: named + inline constraints
deltascope audit \
  --dialect postgresql \
  --sql "create table orders (id bigint primary key, user_id bigint references users(id), amount numeric not null check (amount >= 0), constraint uniq_orders_user unique (user_id), constraint chk_orders_amount check (amount >= 0));"

# Phased migration: set a column default
deltascope audit \
  --dialect postgresql \
  --sql "alter table users alter column status set default 'active';"

# Constraint lifecycle: validate a constraint
deltascope audit \
  --dialect postgresql \
  --sql "alter table users validate constraint chk_amount;"

# Constraint lifecycle: drop a constraint (primary-key mapping applies with metadata)
deltascope audit \
  --dialect postgresql \
  --sql "alter table orders drop constraint orders_pkey;"

VALIDATE CONSTRAINT without a corresponding rule produces a clean audit — it is supported and auditable, but does not guarantee a finding. DROP CONSTRAINT on a primary key triggers existing primary-key rules only when metadata is available; in offline mode it passes through as a normal alter action. The v0.23.0 create-table expansion does not claim full PostgreSQL DDL support and does not add new CLI flags or contracts.

PostgreSQL Primary-Key Audit (v0.37.0)

Starting with v0.37.0, DeltaScope populates primary-key facts for PostgreSQL CREATE TABLE statements. Inline, table-level, named, and composite primary-key declarations flow into the normalized primary-key contract, allowing existing primary-key rules to audit PostgreSQL:

# Inline primary key — triggers ddl.table.primary_key.bigint.require if not BIGINT
deltascope audit \
  --dialect postgresql \
  --format json \
  --sql "create table users (id integer primary key, name text not null);"

Example JSON finding:

{
  "rule_id": "ddl.table.primary_key.bigint.require",
  "level": "warning",
  "message": "primary key column \"id\" is not BIGINT",
  "statement_kind": "ddl"
}
# Composite primary key — triggers ddl.table.primary_key.columns.max_count if over limit
deltascope audit \
  --dialect postgresql \
  --sql "create table order_items (order_id bigint, item_id bigint, quantity int, primary key (order_id, item_id));"

Supported PostgreSQL primary-key forms:

Form Example
Inline id bigint PRIMARY KEY
Table-level PRIMARY KEY (id)
Named CONSTRAINT users_pkey PRIMARY KEY (id)
Composite PRIMARY KEY (a, b)

ddl.table.primary_key.not_null.require does not produce a stable negative case for PostgreSQL — primary-key columns are treated as effectively NOT NULL.

PostgreSQL Unique/Index Audit (v0.38.0)

Starting with v0.38.0, DeltaScope extends index rule coverage to standalone PostgreSQL CREATE INDEX and CREATE UNIQUE INDEX statements for approved btree forms:

deltascope audit \
  --dialect postgresql \
  --format json \
  --sql "CREATE UNIQUE INDEX bad_email_unique ON users (email);"

Example JSON finding:

{
  "rule_id": "ddl.index.unique.prefix.require",
  "level": "warning",
  "message": "unique index \"bad_email_unique\" must use prefix \"uniq_\"",
  "statement_kind": "ddl"
}

Rules now covering PostgreSQL standalone CREATE INDEX:

Rule ID What It Flags
ddl.index.secondary.prefix.require Secondary index name does not start with the required prefix
ddl.index.unique.prefix.require Unique index name does not start with the required prefix
ddl.index.columns.max_count Index spans more columns than the allowed maximum

v0.49.0 extends the PostgreSQL CREATE INDEX path so partial indexes, expression indexes, INCLUDE covering indexes, and non-btree access methods are normalized at a coarse fact level. DeltaScope records access method, included columns, predicate presence, and expression-key presence/count, but it does not render or semantically analyze predicate SQL or expression SQL. Operator classes, NULLS NOT DISTINCT, and live schema index introspection remain out of scope.

PostgreSQL ALTER TABLE ADD CONSTRAINT Audit (v0.39.0)

Starting with v0.39.0, DeltaScope extends unique-index prefix and primary-key rule coverage to PostgreSQL ALTER TABLE ... ADD CONSTRAINT ... UNIQUE and ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY forms:

# ALTER TABLE ADD CONSTRAINT UNIQUE — triggers ddl.alter.add_index.unique.prefix.require if prefix is wrong
deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE users ADD CONSTRAINT bad_email_key UNIQUE (email);"

Example JSON finding:

{
  "rule_id": "ddl.alter.add_index.unique.prefix.require",
  "level": "warning",
  "message": "unique index \"bad_email_key\" must use prefix \"uniq_\"",
  "statement_kind": "ddl"
}
# ALTER TABLE ADD CONSTRAINT PRIMARY KEY — triggers ddl.table.primary_key.bigint.require if not BIGINT
deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE users ADD CONSTRAINT users_pkey PRIMARY KEY (id);"

Rules now covering PostgreSQL ALTER TABLE ... ADD CONSTRAINT:

Rule ID What It Flags
ddl.alter.add_index.unique.prefix.require Unique constraint name does not start with the required prefix
ddl.table.primary_key.bigint.require Primary-key column is not BIGINT
ddl.table.primary_key.columns.max_count Composite primary key exceeds the configured column limit

These reuse existing shared alter-table index and primary-key rule families. No new rule IDs were added. This does not add full PostgreSQL constraint support, metadata-aware constraint introspection, or support for ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY or ALTER TABLE ... ADD CONSTRAINT ... CHECK.

PostgreSQL ALTER TABLE ADD CONSTRAINT FOREIGN KEY Audit (v0.40.0)

Starting with v0.40.0, DeltaScope extends FK rule coverage to PostgreSQL ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY forms:

# ALTER TABLE ADD CONSTRAINT FOREIGN KEY — triggers ddl.table.foreign_key.forbid under default policy
deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE orders ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id);"

Example JSON finding:

{
  "rule_id": "ddl.table.foreign_key.forbid",
  "level": "blocker",
  "message": "foreign key constraints are not allowed",
  "statement_kind": "ddl"
}

Rules now covering PostgreSQL ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY:

Rule ID What It Flags
ddl.table.foreign_key.forbid Foreign key constraints are forbidden under the default policy
ddl.pg.table.foreign_key.cross_schema.advisory Cross-schema FK reference when owning and referenced schemas differ

These reuse existing shared FK rule families. No new rule IDs were added. This does not add live schema FK existence validation, deferrable constraint support, MATCH FULL policy expansion, or MySQL/TiDB behavior changes.

PostgreSQL ALTER TABLE ADD CONSTRAINT CHECK Audit (v0.41.0)

Starting with v0.41.0, DeltaScope extends check constraint naming and NOT VALID advisory rule coverage to PostgreSQL ALTER TABLE ... ADD CONSTRAINT ... CHECK forms:

# ALTER TABLE ADD CONSTRAINT CHECK — triggers ddl.pg.alter.add_check.not_valid.require by default
deltascope audit \
  --dialect postgresql \
  --sql "ALTER TABLE orders ADD CONSTRAINT amount_positive CHECK (amount >= 0);"

Example JSON finding:

{
  "rule_id": "ddl.pg.alter.add_check.not_valid.require",
  "level": "warning",
  "message": "ADD CHECK constraint should use NOT VALID to avoid full table scan with ACCESS EXCLUSIVE lock",
  "statement_kind": "ddl"
}
# ALTER TABLE ADD CONSTRAINT CHECK — triggers naming rule when prefix is configured
deltascope audit \
  --dialect postgresql \
  --config deltascope.yaml \
  --sql "ALTER TABLE orders ADD CONSTRAINT amount_positive CHECK (amount >= 0);"

With a config file enabling ddl.constraint.check.name.prefix.require with prefix: ck_, the above produces:

{
  "rule_id": "ddl.constraint.check.name.prefix.require",
  "level": "warning",
  "message": "check constraint \"amount_positive\" must use prefix \"ck_\"",
  "statement_kind": "ddl"
}

Rules now covering PostgreSQL ALTER TABLE ... ADD CONSTRAINT ... CHECK:

Rule ID What It Flags
ddl.pg.alter.add_check.not_valid.require ADD CHECK constraint should use NOT VALID to avoid full table scan
ddl.constraint.check.name.prefix.require Check constraint name does not start with the required prefix (when configured)
ddl.constraint.check.name.suffix.require Check constraint name does not end with the required suffix (when configured)
ddl.constraint.check.name.contains.require Check constraint name does not contain any configured token (when configured)

These reuse existing shared check naming rule families and the PostgreSQL migration-safety rule. ddl.pg.alter.add_check.not_valid.require was already registered; check naming rules cover the ALTER CHECK path through extended applicability. This does not add live schema CHECK existence validation, NOT VALID validation enforcement, deferred constraint support, or MySQL/TiDB behavior changes.

PostgreSQL NOT VALID Constraint Validation Pairing (v0.42.0)

Starting with v0.42.0, DeltaScope adds a PostgreSQL-only GlobalRule for named CHECK and FOREIGN KEY constraints added with NOT VALID. The rule warns when the same audited SQL batch does not contain a later matching ALTER TABLE ... VALIDATE CONSTRAINT ... using the same schema, table, and constraint name.

deltascope audit \
  --dialect postgresql \
  --format json \
  --sql "ALTER TABLE orders ADD CONSTRAINT chk_orders_amount CHECK (amount >= 0) NOT VALID;"

Example JSON excerpt:

{
  "global_findings": [
    {
      "rule_id": "ddl.pg.alter.not_valid_constraint.validate.require",
      "level": "warning",
      "message": "NOT VALID constraint \"chk_orders_amount\" on table \"orders\" should be followed by VALIDATE CONSTRAINT in the audited migration batch"
    }
  ]
}

The finding is suppressed when the batch includes a later matching validation:

ALTER TABLE orders ADD CONSTRAINT chk_orders_amount CHECK (amount >= 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT chk_orders_amount;

This does not add first-time VALIDATE CONSTRAINT parser support, live database validation-state lookup, cross-file deployment tracking, unnamed-constraint matching, CHECK expression validation, FK referenced-table validation, MySQL/TiDB behavior changes, or a new public API contract.

Default Policy Dialect Isolation (v0.43.0)

Starting with v0.43.0, the shipped default policy isolates rules by --dialect. PostgreSQL audits no longer emit MySQL/TiDB-only rule IDs or MySQL-specific remediation text. MySQL/TiDB audits no longer emit PostgreSQL-only rule IDs.

# PostgreSQL audit — no MySQL-only rules appear
deltascope audit \
  --dialect postgresql \
  --sql "CREATE TABLE users (id bigint PRIMARY KEY, name varchar(64) NOT NULL);"

# MySQL audit — no ddl.pg.* rules appear
deltascope audit \
  --dialect mysql \
  --sql "CREATE TABLE users (id bigint unsigned NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)) ENGINE=InnoDB;"

This does not add new rule IDs, parser features, public API contracts, live schema validation, cross-database tracking, or MySQL/TiDB behavior changes beyond dialect isolation.

Repository Confidence Targets

Target Purpose
make pg-unit-test-gates Run PostgreSQL-tagged unit packages without Docker
make pg-e2e-gates Run Docker-backed PostgreSQL CLI, HTTP, and MCP end-to-end suites
make pg-confidence-gates Combine the canonical PostgreSQL unit + E2E confidence gates
make release-surface-gates VERSION=vX.Y.Z Verify the package/release contract for the tagged release
make release-version-surface-gates VERSION=vX.Y.Z Verify versioned docs/install surfaces, bilingual release notes, and release semantic consistency (census, corpus, rule counts, no-overclaim, no-leak)
make ddl-census-report Print tracked DDL coverage census for MySQL, TiDB, and PostgreSQL — inventory/reporting gate, not a full SQL grammar coverage claim
make ddl-parser-error-feasibility-report Print parser-error feasibility classification for all tracked DDL parser-error cases (MySQL 15, TiDB 9, PostgreSQL 5) — classification/report gate, not parser support or fallback extraction
make parser-error-unsupported-contract-test Run parser-error unsupported contract tests across application, SDK, CLI, HTTP, and MCP surfaces — verifies diagnostic clarity, no findings inferred, no forbidden payload leak; does not add parser support, fallback parsing, or new SQL audit rules
make unsupported-diagnostics-evidence-test Run unsupported diagnostics evidence contract tests across application, SDK, CLI, HTTP, and MCP surfaces — verifies structured diagnostic evidence (classification, reason, action_hint, audited, dialect, guidance_code, evidence_ref) without leaking raw SQL or parser internals; does not add parser support, fallback parsing, or new SQL audit rules

v0.22.0 is the E2E & Release Confidence Pack. It does not add new PostgreSQL SQL rule semantics; it documents and validates the existing PostgreSQL product and release surfaces with canonical repository entrypoints. v0.23.0 then extends the documented PostgreSQL CREATE TABLE coverage while keeping these release-surface gates as the canonical verification path.

Starting with v0.44.0, make release-contract-gates VERSION=vX.Y.Z combines version surface verification, binary version smoke, default policy dialect isolation smoke, and archive verification into a single pre-publish gate. See the release notes for the full gate inventory.

Quiet Mode

--quiet changes markdown output only. With markdown output, DeltaScope suppresses the normal report body and prints each finding as a single line. With --format json, the JSON contract is unchanged.

[blocker] dml.where.require: UPDATE and DELETE statements must include a WHERE clause

This is useful for scripted processing or minimalist CI log output.


deltascope rules

Commands for discovering and inspecting the Rule Catalog. These are read-only metadata lookup commands — they do not execute audits, parse SQL, or call the audit service. rules list summary.total is the Catalog count, not rule_summary.loaded and not the Default Policy key count. Catalog rows may be default-disabled (dml.impact.*) or Default Policy enabled but not Loaded (ddl.constraint.foreign_key.name.* under fk_forbid).

rules list

List rules from the shipped Rule Catalog with optional filters.

Flag Type Default Description
--dialect string (none) Filter by dialect: mysql, tidb, postgresql, or common.
--level string (none) Filter by level: blocker, warning, or notice.
--kind string (none) Filter by kind: ddl or dml.
--category string (none) Case-insensitive category/family substring match.
--search string (none) Case-insensitive search across rule ID, summary, tags, and config key.
--format string text Output format: text or json.
--limit int 0 Limit result count; 0 means no limit.

All filters are optional and combine as AND conditions. Invalid enum values produce a clear validation error and exit code 2. Empty results return success with zero rules.

# All rules
deltascope rules list

# Blocker-level rules
deltascope rules list --level blocker

# PostgreSQL warning rules in JSON
deltascope rules list --dialect postgresql --level warning --format json

# Search by keyword
deltascope rules list --search drop_column

# DDL rules in the alter_table category
deltascope rules list --kind ddl --category alter_table

# Limit output
deltascope rules list --level blocker --limit 5

Example text output:

RULE ID                               LEVEL    DIALECT     KIND  CATEGORY
------------------------------------  -------  ----------  ----  -----------
ddl.alter.drop_column.exists.require  blocker  common      ddl   alter_table
ddl.alter.drop_column.forbid          warning  common      ddl   alter_table
ddl.pg.alter.drop_column.advisory     warning  postgresql  ddl   alter_table
3 rules

Example JSON output:

deltascope rules list --dialect postgresql --level warning --format json
{
  "version": "v0.290.0",
  "summary": {
    "total": 62,
    "returned": 62,
    "filters": { "dialect": "postgresql", "level": "warning" }
  },
  "rules": [
    {
      "rule_id": "ddl.pg.alter.add_check.not_valid.require",
      "level": "warning",
      "dialect": "postgresql",
      "kind": "ddl",
      "category": "alter_table",
      "summary": "Require DDL pg alter add check not valid require",
      "enabled": true,
      "tags": ["ddl", "postgresql", "alter_table", "require"]
    }
  ]
}

rules explain

Show detailed information about a single rule by exact rule ID.

Flag Type Default Description
--format string text Output format: text or json.
# Text output
deltascope rules explain dml.where.require

# JSON output
deltascope rules explain dml.where.require --format json

rules explain does not run an audit and does not parse SQL. It returns static rule metadata from the shipped catalog. The JSON output contains level, not severity.

Unknown rule IDs produce a clear error and exit code 2:

rule "nonexistent_rule" not found

Example text output:

Rule ID:    dml.where.require
Level:      blocker
Enabled:    true
Dialects:   common
Kind:       dml
Category:   dml_safety
Config Key: dml.where.require

Summary:
  Require DML where require

Why:
  The statement is missing a clause, option, or object that the shipped policy requires.

Risk:
  Ignoring this rule can allow high-impact data changes to proceed with less safety review.

Suggestion:
  Add the required clause, option, or object explicitly so the rule no longer has to infer intent.

Tags: dml, common, dml_safety, require
Trigger Example:
  DELETE FROM users;
Valid Example:
  DELETE FROM users WHERE id = 1;

Default Params:
  required: true

Default policy:
  rules:
    dml.where.require:
      enabled: true
      level: blocker
      params:
        required: true

Safe override example:
  rules:
    dml.where.require:
      enabled: true
      level: warning
      params:
        required: true

Inspect effective rule status:
  deltascope config status dml.where.require --config deltascope.yaml

The Default policy: block is the rule's authoritative baseline from the engine. The Safe override example: block is a complete rule policy (it keeps enabled and params, only changing level) — copy it verbatim to change the level without disabling the rule. Inspect effective rule status: hands off to config status so you can confirm what your own config file actually does. Default Params: is the compact params-only view. rules explain --format json is unchanged and still carries the config_example field.

Example JSON output (abbreviated):

{
  "version": "v0.290.0",
  "rule": {
    "rule_id": "dml.where.require",
    "level": "blocker",
    "enabled": true,
    "dialects": ["common"],
    "kind": "dml",
    "category": "dml_safety",
    "summary": "Require DML where require",
    "config_key": "dml.where.require",
    "tags": ["dml", "common", "dml_safety", "require"]
  }
}

Non-Goals

These commands do not:

  • Audit SQL statements
  • Add new audit rules or change rule behavior
  • Change the finding JSON shape
  • Introduce a severity field
  • Provide SDK, HTTP, or MCP rule discovery surfaces

deltascope config

Commands for managing the policy configuration file.

config init

Prints the complete default policy YAML to stdout. Redirect to a file to create a local config:

deltascope config init > deltascope.yaml

The generated file contains every rule with its default enabled state and all parameter values explicitly set. Empty string params are encoded as "" so the file is valid YAML for config lint. Edit it to customize your policy.

config lint

Validates a config file for YAML syntax, valid rule IDs, valid levels, and valid param types, and warns about rule-level replacement hazards. Useful as a pre-commit check or in CI.

Flag Type Default Description
--file string (none) Path to the YAML config file to lint. Required.
--strict bool false Fail (exit 2) when lint warnings are present.
deltascope config lint --file ./deltascope.yaml
deltascope config lint --file ./deltascope.yaml --strict

A clean file prints Config OK and exits 0:

Config OK

When the file is valid but mentions a rule without all of its fields, config lint warns. A common hazard is mentioning a rule only to change its level, which replaces the whole rule policy and turns the rule OFF (see Rule-Level Replacement):

rules:
  dml.where.require:
    level: warning
Config OK with warnings

Warnings:
- dml.where.require is OFF because "enabled" is omitted.
  This config replaces the whole rule policy; it does not merge with defaults.
  Inspect effective rule status:
    deltascope config status dml.where.require --config ./deltascope.yaml
- dml.where.require removes default params because "params" is omitted.
  This config replaces the whole rule policy; it does not merge with defaults.
  Inspect effective rule status:
    deltascope config status dml.where.require --config ./deltascope.yaml

Each warning states the field that was omitted, the consequence, and the replaces the whole rule policy; does not merge with defaults framing, followed by an Inspect effective rule status: line pointing at config status with the same file path you passed to --file. Warnings are advisory. Without --strict, the command still exits 0 after printing them. With --strict, the same text is printed and the command exits 2. To confirm the effective state a warned rule lands in, run the config status command the warning hands you (see config status).

On a validation error the message is printed and the command exits 2. Errors take precedence over warnings, so a file with both an error and a hazard reports only the error:

unknown rule "ddl.table.comments.require"

config lint has no JSON output and does not accept --format. To inspect effective policy as JSON, use config status <rule-id> --format json.

config show-default

Prints the built-in default policy. Equivalent to config init.

deltascope config show-default

config status

Show the effective status of one shipped rule under the current config. It answers: is this rule ON or OFF in Default Policy (or your config), is it Loaded, and which level will it use if it fires? FK naming rules under the shipped baseline stay ON in Default Policy and are not Loaded; config status names that as fk_forbid rather than treating the rules as missing.

deltascope config status <rule-id> [--format text|json]
deltascope --config ./deltascope.yaml config status <rule-id>
deltascope --config ./deltascope.yaml config status <rule-id> --format json
Flag Type Default Description
--format string text Output format: text or json.

The config file is selected with the global --config flag, the same flag audit uses. When --config is omitted, the command reports the built-in default policy and states that no config override is active.

How config status differs from the other rule commands

These three commands answer different questions. Pick by intent:

  • deltascope rules explain <rule-id> explains what the rule means — its summary, why, risk, suggestion, tags, and default params from the shipped catalog. It does not look at your config.
  • deltascope config status <rule-id> shows what your config makes the rule do — whether it is ON or OFF under the active config and which level it will use.
  • deltascope config lint --file validates a config file — YAML shape, valid rule IDs, valid levels, and param types — and warns about rule-level replacement hazards. It does not report effective status for any rule; use config status for that.

Text output

Default policy (no --config):

deltascope config status dml.where.require
Rule: dml.where.require

Current status:
  ON
  Findings from this rule fail as: blocker.

Config effect:
  No config supplied. This rule uses the default policy.

Default:
  enabled: true
  level: blocker
  params:
    required: true

Current:
  enabled: true
  level: blocker
  params:
    required: true

Rule details:
  deltascope rules explain dml.where.require

FK-forbid suppressed naming rule — still in the Rule Catalog and Default Policy, not Loaded:

deltascope config status ddl.constraint.foreign_key.name.prefix.require
Rule: ddl.constraint.foreign_key.name.prefix.require

Current status:
  ON
  Not Loaded: fk_forbid (suppressed by ddl.table.foreign_key.forbid).
  This rule will not produce findings while that suppression applies.

The same three IDs (prefix / suffix / contains) share this reason. They are suppressed, not missing. Disable ddl.table.foreign_key.forbid to Load them.

Full-spec override — every field specified, only level differs, rule stays ON:

rules:
  dml.where.require:
    enabled: true
    level: warning
    params:
      required: true
deltascope --config ./deltascope.yaml config status dml.where.require
Rule: dml.where.require

Current status:
  ON
  Findings from this rule fail as: warning.

Config effect:
  Your config mentions this rule, so it replaces the default rule policy.
  `level` changes from blocker to warning.

Default:
  enabled: true
  level: blocker
  params:
    required: true

Current:
  enabled: true
  level: warning
  params:
    required: true

Rule details:
  deltascope rules explain dml.where.require

Partial config — the dangerous case (see Rule-Level Replacement Semantics). Writing only level mentions the rule, which replaces its whole policy, so the omitted enabled becomes false and the rule ends up OFF:

rules:
  dml.where.require:
    level: warning
deltascope --config ./deltascope.yaml config status dml.where.require
Rule: dml.where.require

Current status:
  OFF
  This rule will not produce findings.

Config effect:
  Your config mentions this rule, so it replaces the default rule policy.
  `enabled` is omitted, so the effective value is false.
  `level` changes from blocker to warning.
  `params.required` is removed.
  This rule is OFF.

Default:
  enabled: true
  level: blocker
  params:
    required: true

Current:
  enabled: false
  level: warning
  params:
    (none)

Rule details:
  deltascope rules explain dml.where.require

Text output is intended for humans. For automation, use JSON.

JSON output

--format json returns a stable wrapper. The public priority field is level; there is no severity field.

deltascope config status dml.where.require --format json
{
  "version": "v0.310.0",
  "rule_id": "dml.where.require",
  "status": {
    "enabled": true,
    "level": "blocker",
    "state": "on",
    "loaded": true
  },
  "default": {
    "enabled": true,
    "level": "blocker",
    "params": {
      "required": true
    }
  },
  "current": {
    "enabled": true,
    "level": "blocker",
    "params": {
      "required": true
    }
  },
  "config_effect": {
    "has_config": false,
    "has_override": false,
    "changed_fields": [],
    "messages": [
      "No config supplied. This rule uses the default policy."
    ]
  },
  "rule_details_command": "deltascope rules explain dml.where.require"
}

Required JSON fields:

Field Meaning
version DeltaScope build version
rule_id Requested rule ID
status.enabled Effective enabled state (Default Policy or config)
status.level Effective rule level
status.state on or off
status.loaded Whether the rule is registered for audit. Distinct from Catalog and from status.enabled.
suppression Present when an enabled Default Policy rule is not Loaded. reason is fk_forbid for the three ddl.constraint.foreign_key.name.* rules; by is ddl.table.foreign_key.forbid.
default Built-in default policy values for the rule
current Effective values after config loading
config_effect.has_config Whether global --config was supplied
config_effect.has_override Whether the requested rule was mentioned in the config file
config_effect.changed_fields Changed fields, such as enabled, level, or params.required
config_effect.messages Human-readable explanation lines
rule_details_command deltascope rules explain <rule_id>

Errors

The command exits with code 2 (bad user input) when:

  • <rule-id> is missing or more than one positional arg is supplied.
  • <rule-id> is not a shipped rule (rule "not.real.rule" not found).
  • --format is not text or json.
  • --config points to a missing, unreadable, or invalid YAML file.
  • The config contains an unknown rule, an invalid level, an unknown param, or a param type mismatch. config status reuses config lint semantics, so it never silently accepts a malformed config.

Non-Goals

config status does not:

  • Run an audit or parse SQL.
  • Connect to a database.
  • Change audit behavior, rule behavior, or the finding JSON shape.
  • Add a severity field.
  • Add SDK, HTTP, or MCP config-status surfaces.
  • Print the status of every rule at once. A future config effective command for bulk inspection is out of scope for this release.

deltascope capabilities

Prints a human-readable summary of all registered capabilities, rule families, and supported dialects. Useful for verifying what a particular build of DeltaScope supports.

deltascope capabilities

deltascope ddl-coverage

Query the generated DDL coverage catalog for verified DeltaScope entries. The catalog is compiled into the binary, so the command works from any working directory and does not require a source checkout. This is a catalog lookup command — it does not execute audits, parse SQL, or call the audit service.

Synopsis

deltascope ddl-coverage [flags]

Flags

Flag Default Description
--dialect (none) Filter by dialect: mysql, tidb, postgresql
--classification (none) Filter by classification: finding_covered, normalized_silent, unsupported_boundary, parser_error, unclassified
--guidance-code (none) Filter by guidance code: parser_upgrade_candidate
--family (none) Case-insensitive substring match on catalog family
--form (none) Case-insensitive substring match on catalog form
--search (none) Case-insensitive substring match across family, form, notes, guidance code, and rule IDs
--format text Output format: text or json
--limit 0 Limit result count; 0 means no limit

All filters are optional. Multiple filters combine as AND conditions.

Examples

# MySQL parser-upgrade candidates
deltascope ddl-coverage --dialect mysql --classification parser_error --guidance-code parser_upgrade_candidate

# PostgreSQL DROP SUBSCRIPTION in JSON
deltascope ddl-coverage --dialect postgresql --search "drop subscription" --format json

# All TiDB entries in JSON
deltascope ddl-coverage --dialect tidb --format json

# Empty lookup returns success with entries: []
deltascope ddl-coverage --search definitely-not-present --format json

Output Formats

Text output (default) prints a column-aligned table with DIALECT, CLASSIFICATION, FAMILY, FORM, and GUIDANCE columns, followed by a count.

JSON output (--format json) returns a stable machine-readable contract:

{
  "version": "v0.280.0",
  "summary": {
    "total": 2,
    "returned": 2,
    "filters": { "dialect": "mysql" }
  },
  "entries": [...]
}

Non-Goals

This command does not:

  • Audit SQL statements
  • Add parser support or fallback parser behavior
  • Add new SQL audit rules
  • Claim full DDL support or dialect parity
  • Claim vendor grammar completeness

Query results reflect verified catalog entries. An empty result means no catalog match — not a failure, and not a statement about database support. See ddl-coverage.md and ddl-coverage-catalog.json for full catalog details.


deltascope version

Prints the full version string, including compiled dialects. Release ldflags builds print the release tag. Untagged source builds and go install ...@main print the Go module version or VCS revision (devel-<rev> / devel-<rev>-dirty) instead of the last shipped DefaultVersion.

deltascope version

You can also use the global flag form to get just the version and exit from any invocation:

deltascope --version

Exit Codes

audit and query-access analyze use different exit tables. Do not treat a Query Access exit as an audit Fail Threshold result. deltascope audit --help and deltascope query-access analyze --help print the same tables.

audit

Exit Code Meaning
0 Audit completed and findings are below the --fail-on threshold; or a non-audit, non-Query-Access command completed successfully.
1 Audit completed and at least one finding met or exceeded the --fail-on threshold.
2 User error: invalid flags, malformed SQL, empty --sql, unreadable or invalid config file, conflicting --dialect, or ambiguous schema resolution.
3 Runtime or internal failure (unexpected error, connection failure in metadata-aware mode, etc.).

Empty explicit --sql prints audit: SQL input must not be empty and exits 2 without reading stdin.

query-access analyze

Exit Code Meaning
0 Admissible.
1 Rejected.
2 Indeterminate admission.
3 Usage or connection error, including empty --sql.

Empty explicit --sql prints query-access: SQL input must not be empty and exits 3 without reading stdin.


Release Validation

Starting with v0.25.0, DeltaScope release validation includes SQL corpus tests that run representative MySQL, TiDB, and PostgreSQL cases through the audit application layer with two-layer assertions (report-level and semantic). These corpus tests are release-confidence assets and do not affect CLI behavior or require any user action.

PostgreSQL ALTER TABLE GENERATED Follow-up Pack (v0.31.0)

Starting with v0.31.0, additional PostgreSQL generated/identity ALTER TABLE forms are surfaced as explicit unsupported boundaries, closing the adjacent gap left by v0.30.0. The CLI exposes these through the same unsupported result path: the audit output includes an unsupported array with feature and reason fields, and the process exits with the audit exit code.

  • ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSIONgenerated_column
  • ALTER TABLE ... ALTER COLUMN ... SET GENERATED ...generated_as_identity
  • ALTER TABLE ... ALTER COLUMN ... DROP IDENTITYgenerated_as_identity
  • Corpus, service, and CLI / HTTP / MCP / pkg/deltascope parity lock the same contract.
  • This is not a new CLI flag or support expansion — it is boundary tightening.

PostgreSQL ALTER TABLE GENERATED Boundary Pack (v0.30.0)

Starting with v0.30.0, PostgreSQL ALTER TABLE ... ADD COLUMN forms that carry generated stored or identity semantics are surfaced as explicit unsupported boundaries. The CLI exposes these through the same unsupported result path: the audit output includes an unsupported array with feature and reason fields, and the process exits with the audit exit code.

  • ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) STOREDgenerated_column
  • ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS IDENTITYgenerated_as_identity
  • Corpus, service, and CLI / HTTP / MCP / pkg/deltascope parity lock the same contract.
  • Adjacent DROP EXPRESSION, SET GENERATED, and DROP IDENTITY forms now receive explicit unsupported mappings in v0.31.0.
  • This is not a new CLI flag or support expansion — it is boundary tightening.

PostgreSQL CREATE TABLE Unsupported Boundaries (v0.26.0)

Starting with v0.26.0, the PostgreSQL extractor explicitly rejects identity columns, generated stored columns, exclusion constraints, and partitioned tables as unsupported boundaries. The CLI exposes these through the unsupported result path: the audit output includes an unsupported array with feature and reason fields, and the process exits with the audit exit code. This is not a new CLI flag or contract — it is a boundary tightening that ensures these forms are no longer silently accepted or partially handled.

Schema-Qualified Reference Semantics (v0.27.0)

Starting with v0.27.0, the PostgreSQL extractor preserves schema-qualified referenced-object facts (ReferencedSchema) in the shared contract. Starting with v0.28.0, FK forbid finding metadata now exposes these referenced-object fields. This is not a new CLI flag or output contract.

Referenced-Object Metadata Surface (v0.28.0)

Starting with v0.28.0, the ddl.table.foreign_key.forbid finding metadata in CLI JSON output now includes referenced_schema (e.g., "public"), referenced_table (e.g., "users"), and referenced_columns (e.g., ["id"]) when the underlying PostgreSQL FK constraint carries those facts. This is an additive metadata widening — no new CLI flags, no new output contract fields beyond the finding metadata object. referenced_table is never concatenated with referenced_schema (e.g., never "public.users").

Example finding metadata for a schema-qualified FK:

{
  "rule_id": "ddl.table.foreign_key.forbid",
  "level": "blocker",
  "message": "...",
  "metadata": {
    "table": "orders",
    "constraint": "fk_orders_approver",
    "columns": ["approver_id"],
    "referenced_schema": "public",
    "referenced_table": "users",
    "referenced_columns": ["id"]
  }
}

This is not a new CLI flag, not schema-aware FK policy support, and not a new rule family.

Schema-Aware FK Policy Pack (v0.29.0)

Starting with v0.29.0, CLI JSON output can also surface the PostgreSQL-only notice rule ddl.pg.table.foreign_key.cross_schema.advisory for explicit cross-schema foreign keys.

  • The rule fires only when the owning table schema and referenced schema are both explicit and different.
  • Same-schema foreign keys do not trigger it.
  • Bare references such as REFERENCES users(id) remain schema unknown and do not trigger it.
  • DeltaScope does not infer public and does not model PostgreSQL search_path.
  • No new CLI flag is introduced.

Example notice-level finding metadata for an explicit cross-schema FK:

{
  "rule_id": "ddl.pg.table.foreign_key.cross_schema.advisory",
  "level": "notice",
  "message": "...",
  "metadata": {
    "table": "orders",
    "table_schema": "billing",
    "constraint": "fk_orders_approver",
    "columns": ["approver_id"],
    "referenced_schema": "auth",
    "referenced_table": "users",
    "referenced_columns": ["id"]
  }
}

referenced_table remains normalized as "users", never "auth.users".


Cross-References


v0.36.0: Rule Coverage for Generated/Identity State-Transition Forms

Starting with v0.36.0, PostgreSQL generated/identity state-transition forms that were supported in v0.35.0 now produce explicit rule_id findings. The supported parser/output path is unchanged — the difference is that these forms now trigger PostgreSQL-only forbid rules instead of passing silently.

New rule IDs:

Rule ID Covered Form
ddl.alter.drop_expression.forbid ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
ddl.alter.set_generated.forbid ALTER TABLE ... ALTER COLUMN ... SET GENERATED ...
ddl.alter.drop_identity.forbid ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY

Example JSON finding:

{
  "findings": [
    {
      "rule_id": "ddl.alter.set_generated.forbid",
      "level": "blocker",
      "message": "ALTER action 'set_generated' is not allowed"
    }
  ]
}

This is rule coverage — not parser support widening, not spec contract widening, not generated expression evaluation, not complete PostgreSQL sequence semantics. No new CLI flags were added.

v0.35.0: State-Transition Support for Generated/Identity Columns

Starting with v0.35.0, PostgreSQL state-transition forms for generated and identity columns are processed through the normal supported audit path. CLI output for these forms no longer includes an unsupported array — instead, the audit produces normal results with findings where applicable.

Supported forms:

  • ALTER TABLE ... ALTER COLUMN ... DROP EXPRESSION
  • ALTER TABLE ... ALTER COLUMN ... SET GENERATED ALWAYS
  • ALTER TABLE ... ALTER COLUMN ... SET GENERATED BY DEFAULT
  • ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY

These forms now produce standard CLI output (exit code 0 for clean pass, 1 if findings meet the --fail-on threshold). The normalized contract is: drop_expression, set_generated with generated_when ("a" / "d"), drop_identity.

This is state-transition support — not full generated-column lifecycle support, not generated expression evaluation, not complete PostgreSQL sequence semantics. No new CLI flags were added.

v0.34.0: Narrow Support for Generated/Identity Definitions

Starting with v0.34.0, narrow PostgreSQL generated/identity definition forms are processed through the normal supported audit path. Shared facts (generated_when, is_identity, identity_options) from v0.33.0 continue flowing through the normal result path. No new CLI flags were added.

v0.33.0: Unsupported Metadata for Generated/Identity Statements

Starting with v0.33.0, PostgreSQL unsupported generated/identity outcomes carry structured metadata in CLI JSON output. When auditing a CREATE TABLE or ALTER TABLE ADD COLUMN statement that contains GENERATED ALWAYS AS (...) STORED or GENERATED ... AS IDENTITY, the unsupported array entry now includes a metadata object:

{
  "unsupported": [
    {
      "feature": "generated_as_identity",
      "reason": "...",
      "metadata": {
        "column": "id",
        "generated_when": "a",
        "is_identity": true,
        "identity_options": { "start": 10, "increment": 5, "cache": 20, "cycle": true }
      }
    }
  ]
}

Metadata keys:

Key Present When Type
column always string
generated_when always "a" (ALWAYS) or "d" (BY DEFAULT)
is_identity identity columns boolean (true)
identity_options identity with options object with numeric/boolean values

This is an additive metadata widening on the unsupported contract. No new CLI flags or output format changes were introduced.