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.
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.
Audit one or more SQL statements from inline text, a file, or standard 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.sqlThese 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. |
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/-pflag has been removed. Use--password-env,--password-file, or--ask-passwordinstead. Scripts that passed--passwordon 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--dialectis also set explicitly and conflicts, the command exits with code 2.--databaseselects 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--portis omitted, this path keeps the MySQL-oriented default of3306. - For PostgreSQL: pass
--dialect postgresqlexplicitly and use--databaseto select the database (postgreswhen omitted).--schemaselects the schema within that database; when--schemais explicitly set,--databaseis required and is never inferred from the schema value. Omitting both preserves default catalog resolution. When--portis omitted, the explicit PostgreSQL selection uses5432; 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 →
--schemaflag → 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-passwordis 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.sqlHuman-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`
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, ornotice`) and the deduplicated finding countSummary:andSuggestion:— rule catalog text when available, otherwise the first finding's message and suggestionExplain: deltascope rules explain <rule_id>— a copy-paste command to inspect the ruleStatements:— 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_summaryfield, and the finding JSON shape is unchanged. levelremains the priority field; noseverityfield 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 jsonand aggregate findings yourself.
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"]
}
}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.
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 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.
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-actionsEach 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 trailingExplain: 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.
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.
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.sarifThe output includes rule metadata (help text from explanation suggestions) under tool.driver.rules and maps severity levels to SARIF levels: blocker → error, warning → warning, notice → note. When --file is provided, each result includes artifactLocation.uri, startLine, and startColumn pointing at the exact statement.
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.jsonIn .gitlab-ci.yml, publish the report as an artifact:
artifacts:
reports:
codequality: gl-code-quality-report.json
when: alwaysField 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.
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 Catalog —
deltascope rules list/list_rules. Includes default-disabled catalog-only rules such asdml.impact.*. - Default Policy — the enabled-rule set used when the caller supplies no config. It does not enable
dml.impact.*. - Loaded —
rule_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 threeddl.constraint.foreign_key.name.*rules stay in Default Policy but are not Loaded whileddl.table.foreign_key.forbidis enabled;config statusreports that as reasonfk_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.
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.
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.
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.
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.
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.
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_candidateidentifies 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_candidateis a documented classification, not a new CLI field. - The
parser_errordiagnostic still means the statement was not audited. Users should not treatparser_erroras PASS. - Users should review parser-error statements manually.
- No promise that future versions will support these syntax forms.
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 isparser_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.
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 VALID → VALIDATE 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.
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.
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.
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.
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.
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.
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.
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.
| 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 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.
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).
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 5Example 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"]
}
]
}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 jsonrules 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"]
}
}These commands do not:
- Audit SQL statements
- Add new audit rules or change rule behavior
- Change the finding JSON shape
- Introduce a
severityfield - Provide SDK, HTTP, or MCP rule discovery surfaces
Commands for managing the policy configuration file.
Prints the complete default policy YAML to stdout. Redirect to a file to create a local config:
deltascope config init > deltascope.yamlThe 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.
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 --strictA 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: warningConfig 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.
Prints the built-in default policy. Equivalent to config init.
deltascope config show-defaultShow 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.
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 whichlevelit will use.deltascope config lint --filevalidates 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; useconfig statusfor that.
Default policy (no --config):
deltascope config status dml.where.requireRule: 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.requireRule: 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: truedeltascope --config ./deltascope.yaml config status dml.where.requireRule: 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: warningdeltascope --config ./deltascope.yaml config status dml.where.requireRule: 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.
--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> |
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).--formatis nottextorjson.--configpoints 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 statusreusesconfig lintsemantics, so it never silently accepts a malformed config.
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
severityfield. - Add SDK, HTTP, or MCP config-status surfaces.
- Print the status of every rule at once. A future
config effectivecommand for bulk inspection is out of scope for this release.
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 capabilitiesQuery 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.
deltascope ddl-coverage [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.
# 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 jsonText 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": [...]
}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.
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 versionYou can also use the global flag form to get just the version and exit from any invocation:
deltascope --versionaudit 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.
| 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.
| 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.
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.
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 EXPRESSION→generated_columnALTER TABLE ... ALTER COLUMN ... SET GENERATED ...→generated_as_identityALTER TABLE ... ALTER COLUMN ... DROP IDENTITY→generated_as_identity- Corpus, service, and CLI / HTTP / MCP /
pkg/deltascopeparity lock the same contract. - This is not a new CLI flag or support expansion — it is boundary tightening.
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 (...) STORED→generated_columnALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS IDENTITY→generated_as_identity- Corpus, service, and CLI / HTTP / MCP /
pkg/deltascopeparity lock the same contract. - Adjacent
DROP EXPRESSION,SET GENERATED, andDROP IDENTITYforms now receive explicit unsupported mappings inv0.31.0. - This is not a new CLI flag or support expansion — it is boundary tightening.
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.
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.
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.
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
publicand does not model PostgreSQLsearch_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".
- Rule catalog — rules.md
- Policy configuration — config.md
- HTTP API — http-api.md
- Metadata-aware mode — ../concept/metadata-aware-mode.md
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.
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 EXPRESSIONALTER TABLE ... ALTER COLUMN ... SET GENERATED ALWAYSALTER TABLE ... ALTER COLUMN ... SET GENERATED BY DEFAULTALTER 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.
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.
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.