<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fan()</title>
    <description>The latest articles on DEV Community by Fan() (@fanduzi).</description>
    <link>https://dev.to/fanduzi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3922190%2F1adc84a3-39f9-41db-a653-af86815b6536.png</url>
      <title>DEV Community: Fan()</title>
      <link>https://dev.to/fanduzi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fanduzi"/>
    <language>en</language>
    <item>
      <title>Why PostgreSQL ADD COLUMN NOT NULL DEFAULT now() should not pass review on sight</title>
      <dc:creator>Fan()</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:51:59 +0000</pubDate>
      <link>https://dev.to/fanduzi/why-postgresql-add-column-not-null-default-now-should-not-pass-review-on-sight-k3d</link>
      <guid>https://dev.to/fanduzi/why-postgresql-add-column-not-null-default-now-should-not-pass-review-on-sight-k3d</guid>
      <description>&lt;p&gt;&lt;em&gt;Adrian Van (GitHub: Fanduzi)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last week a change ticket landed with one statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer said: add a column, give it a default, old rows will not be NULL. The ticket was clean. The style check passed. Someone had already booked a low-traffic window.&lt;/p&gt;

&lt;p&gt;I stopped it. The SQL is valid. The ticket still does not say which PostgreSQL version, whether the default is &lt;code&gt;STABLE&lt;/code&gt; or &lt;code&gt;VOLATILE&lt;/code&gt;, or whether every old row should share the ALTER's timestamp. Get those wrong and a large heap takes a long &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, a WAL spike, and as much as another copy of the table on disk. It looks like an add-column. It can run like a rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a column is not always cheap
&lt;/h2&gt;

&lt;p&gt;PostgreSQL does not treat every &lt;code&gt;ADD COLUMN&lt;/code&gt; the same.&lt;/p&gt;

&lt;p&gt;A nullable column with no default is cheap on current versions: catalog only, heap barely touched. PostgreSQL 11 also made a &lt;strong&gt;non-volatile&lt;/strong&gt; default cheap. The value is evaluated once, stored in the catalog (&lt;code&gt;atthasmissing&lt;/code&gt; / &lt;code&gt;attmissingval&lt;/code&gt;), and returned for existing rows. &lt;code&gt;relfilenode&lt;/code&gt; does not change. The exclusive lock is short.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEFAULT 0&lt;/code&gt; is that path.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DEFAULT now()&lt;/code&gt; is not a constant. It is a function call. That is the first reason a reviewer should pause. The second is volatility. The &lt;a href="https://www.postgresql.org/docs/current/sql-altertable.html" rel="noopener noreferrer"&gt;ALTER TABLE notes&lt;/a&gt; are specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A non-volatile default is stored in metadata. No rewrite.&lt;/li&gt;
&lt;li&gt;A volatile default rewrites the table &lt;strong&gt;and its indexes&lt;/strong&gt;. The documented example is &lt;code&gt;clock_timestamp()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;now()&lt;/code&gt; is an alias for &lt;code&gt;transaction_timestamp()&lt;/code&gt;. It is &lt;code&gt;STABLE&lt;/code&gt;: one value for the whole transaction. On PostgreSQL 11 and later, &lt;code&gt;NOT NULL DEFAULT now()&lt;/code&gt; usually takes the catalog path. &lt;code&gt;clock_timestamp()&lt;/code&gt;, &lt;code&gt;timeofday()&lt;/code&gt;, and &lt;code&gt;random()&lt;/code&gt; are &lt;code&gt;VOLATILE&lt;/code&gt;. Those still rewrite.&lt;/p&gt;

&lt;p&gt;Before PostgreSQL 11, a default of any kind rewrote the table.&lt;/p&gt;

&lt;p&gt;So the one-liner is three different operations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Typical PostgreSQL 11+ behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Constant (&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;'x'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Catalog only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;STABLE&lt;/code&gt; function (&lt;code&gt;now()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Catalog only. Every old row reads as the ALTER's timestamp.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VOLATILE&lt;/code&gt; function (&lt;code&gt;clock_timestamp()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Table + index rewrite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A rewrite changes &lt;code&gt;relfilenode&lt;/code&gt;. It holds &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; for the whole build. Disk can approach 2× table size while the new heap is written. Rewriting forms of &lt;code&gt;ALTER TABLE&lt;/code&gt; are not MVCC-safe: a concurrent transaction whose snapshot started before the rewrite can see the table as empty.&lt;/p&gt;

&lt;p&gt;That is not a style question. It is "will production rebuild this heap, and what timestamp do the old rows get."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tickets still sail through
&lt;/h2&gt;

&lt;p&gt;Change tickets look at grants, backups, and the window. They do not always remember that &lt;code&gt;DEFAULT now()&lt;/code&gt; and &lt;code&gt;DEFAULT 0&lt;/code&gt; are not the same class of work, or that &lt;code&gt;now()&lt;/code&gt; and &lt;code&gt;clock_timestamp()&lt;/code&gt; differ.&lt;/p&gt;

&lt;p&gt;A style checker is happy. Indentation is fine. &lt;code&gt;now()&lt;/code&gt; is just a function call. Passing a linter is not an assessment of lock time.&lt;/p&gt;

&lt;p&gt;Online schema-change tools answer &lt;em&gt;how&lt;/em&gt; to apply a change. They do not answer &lt;em&gt;whether this shape should run&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Another miss, same family: a change split into several "small" statements. On MySQL, two &lt;code&gt;ALTER TABLE&lt;/code&gt;s against the same table can be two rebuilds. Each line looks harmless. The ticket passes them one by one. Nobody reviews the pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same class of legal-but-expensive DDL
&lt;/h2&gt;

&lt;p&gt;I stop these on sight too. None of them are syntax errors. All of them are expensive when the table is large.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CHECK without &lt;code&gt;NOT VALID&lt;/code&gt;.&lt;/strong&gt; Adding a CHECK takes &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; and scans the table. Safer: &lt;code&gt;ADD CONSTRAINT … NOT VALID&lt;/code&gt;, then &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; (that validation takes &lt;code&gt;SHARE UPDATE EXCLUSIVE&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;amount_positive&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;CREATE INDEX&lt;/code&gt; without &lt;code&gt;CONCURRENTLY&lt;/code&gt;.&lt;/strong&gt; The default build blocks writes. On a live table that is the wrong default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;DELETE&lt;/code&gt; without &lt;code&gt;WHERE&lt;/code&gt;.&lt;/strong&gt; Everyone already treats that as a red line. That is why the ALTER comes first in this note. A bare &lt;code&gt;DELETE&lt;/code&gt; is hard to sneak through a ticket. &lt;code&gt;DEFAULT now()&lt;/code&gt; is easy to click through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want written on the ticket
&lt;/h2&gt;

&lt;p&gt;If the table is small and the window is real, a rewrite can be fine. Write that down. Do not assume it from the SQL text.&lt;/p&gt;

&lt;p&gt;Check &lt;code&gt;relfilenode&lt;/code&gt; before and after on a clone if you are unsure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;relfilenode&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public.users'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it changed, the statement rewrote the heap.&lt;/p&gt;

&lt;p&gt;Check volatility if the default is a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;proname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provolatile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_proc&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;proname&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'clock_timestamp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'transaction_timestamp'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- i = immutable, s = stable, v = volatile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then answer the semantic question. &lt;code&gt;created_at&lt;/code&gt; on every old row equal to the time of the ALTER is usually a lie. If that is acceptable and you are on PostgreSQL 11+ with &lt;code&gt;now()&lt;/code&gt;, the one-liner is cheap. Still write the version and the function name on the ticket.&lt;/p&gt;

&lt;p&gt;If old rows should not share one timestamp, do not use the one-liner:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE users ADD COLUMN created_at timestamptz;&lt;/code&gt; — catalog only.&lt;/li&gt;
&lt;li&gt;Backfill in batches. Not one &lt;code&gt;UPDATE&lt;/code&gt; of the whole table.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE users ALTER COLUMN created_at SET NOT NULL;&lt;/code&gt; — scan, no rewrite. Or add &lt;code&gt;NOT NULL … NOT VALID&lt;/code&gt; and &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE users ALTER COLUMN created_at SET DEFAULT now();&lt;/code&gt; — future inserts only.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If someone wrote &lt;code&gt;clock_timestamp()&lt;/code&gt;, or the cluster is older than 11, plan a rewrite. Do not discover it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  An offline check that flags the shape
&lt;/h2&gt;

&lt;p&gt;I also run the SQL text through an offline linter before the window. DeltaScope v0.490.0 (rule catalog 371: blocker 72, warning 142, notice 157) parses the statement and applies policy. It does not execute the SQL. It does not run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. Default dialect is MySQL; PostgreSQL needs &lt;code&gt;--dialect postgresql&lt;/code&gt;. Without a database connection it does not check that the table exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dialect&lt;/span&gt; postgresql &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE users ADD COLUMN created_at timestamptz NOT NULL DEFAULT now()"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That hits &lt;code&gt;ddl.pg.alter.add_column.non_null_default.rewrite.warn&lt;/code&gt;. Default level is warning. Verdict is &lt;strong&gt;review&lt;/strong&gt;, not reject. Extra facts on the finding: &lt;code&gt;not_null&lt;/code&gt;, &lt;code&gt;has_default&lt;/code&gt;, &lt;code&gt;default_kind&lt;/code&gt; (here &lt;code&gt;function_call&lt;/code&gt;). The linter is conservative: a function-call default plus &lt;code&gt;NOT NULL&lt;/code&gt; is enough to flag. It cannot see server version or &lt;code&gt;provolatile&lt;/code&gt; from the text alone.&lt;/p&gt;

&lt;p&gt;I would not default that rule to a hard fail. A 2k-row table in a maintenance window can rewrite. A few-hundred-million-row table with the same SQL is an incident. The tool marks the shape. Window and rollback stay a human call.&lt;/p&gt;

&lt;p&gt;The same pass will also flag CHECK without &lt;code&gt;NOT VALID&lt;/code&gt; and &lt;code&gt;CREATE INDEX&lt;/code&gt; without &lt;code&gt;CONCURRENTLY&lt;/code&gt;. That is all I use it for here. A reader who never installs it still has the rewrite lesson above.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/Fanduzi/DeltaScope" rel="noopener noreferrer"&gt;https://github.com/Fanduzi/DeltaScope&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Site: &lt;a href="https://deltascope.pages.dev/" rel="noopener noreferrer"&gt;https://deltascope.pages.dev/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
License: Apache 2.0&lt;/p&gt;

&lt;p&gt;If you do connect it to a database, use &lt;code&gt;--ask-password&lt;/code&gt;, &lt;code&gt;--password-env&lt;/code&gt;, or &lt;code&gt;--password-file&lt;/code&gt;. Not &lt;code&gt;--password&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The statement parses. The ticket is clean. You still stop and ask: which PostgreSQL version, which function, does every old row get the ALTER time, and can this table absorb a rewrite.&lt;/p&gt;

&lt;p&gt;If the answer is fuzzy, it does not pass on sight.&lt;/p&gt;




&lt;p&gt;Also published in Chinese: &lt;a href="https://juejin.cn/post/7677795752349827072" rel="noopener noreferrer"&gt;Juejin&lt;/a&gt;, &lt;a href="https://www.modb.pro/db/2092275245131046912" rel="noopener noreferrer"&gt;MODB&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
    </item>
    <item>
      <title>Automated SQL Migration Review with Real CLI Output (MySQL, PostgreSQL, TiDB)</title>
      <dc:creator>Fan()</dc:creator>
      <pubDate>Sat, 09 May 2026 16:58:27 +0000</pubDate>
      <link>https://dev.to/fanduzi/automated-sql-migration-review-with-real-cli-output-mysql-postgresql-tidb-17b5</link>
      <guid>https://dev.to/fanduzi/automated-sql-migration-review-with-real-cli-output-mysql-postgresql-tidb-17b5</guid>
      <description>&lt;h1&gt;
  
  
  Auditing MySQL ALTER TABLE Risks with a CLI (Real Output Included)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I'm the author of &lt;a href="https://github.com/Fanduzi/DeltaScope" rel="noopener noreferrer"&gt;DeltaScope&lt;/a&gt;, an open-source offline SQL audit tool that supports MySQL, TiDB, and PostgreSQL. This post skips the marketing and shows real SQL inputs with real audit outputs — no fabrications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 1: An Innocent-Looking Migration File
&lt;/h2&gt;

&lt;p&gt;Consider a migration file &lt;code&gt;migration.sql&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Add columns and index to users table&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_phone&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Clean up temp data&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temp_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Audit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--file&lt;/span&gt; migration.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: reject

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

## Statement 1
- SQL: ALTER TABLE users ADD COLUMN phone VARCHAR(20)
No findings.

## Statement 2
- SQL: ALTER TABLE users ADD COLUMN age INT DEFAULT 0
No findings.

## Statement 3
- SQL: ALTER TABLE users ADD INDEX idx_phone (phone)
No findings.

## Statement 4
- SQL: DELETE FROM temp_data

### Findings
- [blocker] dml.where.require: UPDATE and DELETE statements must include a WHERE clause
  Suggestion: add a WHERE clause that narrows the affected rows

### Impact
- estimated_ratio: 1.0000
- risk_level: high
- confidence: high
- source: shape
- reason_code: missing_where

## Global Findings
- [warning] ddl.alter.merge.mysql.require: multiple ALTER TABLE statements
  target "users" under mysql mode
  alter_count: 3, dialect: mysql
  Suggestion: merge repeated alter statements on the same table into
  a single ALTER TABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;DELETE FROM temp_data&lt;/code&gt; has no WHERE clause — offline mode estimates ratio = 1.0 (full table), flagged as high risk&lt;/li&gt;
&lt;li&gt;Three ALTER statements target the same table &lt;code&gt;users&lt;/code&gt; — MySQL recommends merging them&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After fixing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_phone&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;temp_data&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-audit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: pass
- Statements: 2 | Blockers: 0 | Warnings: 0 | Notices: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scenario 2: Changing Column Type + NULL Constraint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE orders MODIFY COLUMN amount VARCHAR(100) NOT NULL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: reject

- [blocker] ddl.alter.modify_column.explicit_nullability_change.forbid:
  ALTER TABLE modify column explicitly changes nullability for "amount",
  which this policy forbids
  Suggestion: keep nullability unchanged for "amount" or relax the policy
  intentionally after review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement changes the column type from INT to VARCHAR and also changes the NULL constraint. Type changes may trigger a full table COPY in InnoDB (depending on the direction), and NULL constraint changes can silently break application logic that depends on the constraint.&lt;/p&gt;

&lt;p&gt;The rule ID is &lt;code&gt;ddl.alter.modify_column.explicit_nullability_change.forbid&lt;/code&gt;, default level blocker. Teams that genuinely need this change can adjust the level or add an approval flow in the config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 3: Dropping NOT NULL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;MODIFY&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NULL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: reject

- [blocker] ddl.alter.modify_column.explicit_nullability_change.forbid:
  ALTER TABLE modify column explicitly changes nullability for "email",
  which this policy forbids
  Suggestion: keep nullability unchanged for "email" or relax the policy
  intentionally after review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rule as Scenario 2. After dropping NOT NULL, application code like &lt;code&gt;if user.Email != ""&lt;/code&gt; silently breaks — NULL is not an empty string. This won't throw errors at deploy time, but behavior changes quietly, making it expensive to debug later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 4: Dropping Primary Key
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE users DROP PRIMARY KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: reject

- [blocker] ddl.alter.drop_primary_key.forbid:
  ALTER TABLE drop primary key is forbidden for "primary"
  Suggestion: avoid drop primary key in this change or relax the policy intentionally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;InnoDB uses the primary key as the clustered index. Dropping it forces a full table rebuild. Default policy rejects this outright. If you need to change the primary key scheme, the correct approach is to ADD the new primary key column first, then DROP the old one in a separate step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 5: Sloppy CREATE TABLE
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="nb"&gt;unsigned&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;utf8mb4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This SQL executes fine, but the audit finds 8 issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"CREATE TABLE t1 (id bigint unsigned NOT NULL AUTO_INCREMENT, name varchar(100), PRIMARY KEY(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: review
- Statements: 1 | Blockers: 0 | Warnings: 8

- [warning] ddl.table.comment.require: table comment is required
- [warning] ddl.table.audit_columns.require: should include a created-time
  audit column with DEFAULT CURRENT_TIMESTAMP
- [warning] ddl.table.audit_columns.require: should include an updated-time
  audit column with DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- [warning] ddl.column.comment.require: column "id" must include a comment
- [warning] ddl.column.comment.require: column "name" must include a comment
- [warning] ddl.column.default.require: column "id" should define a default value
- [warning] ddl.column.default.require: column "name" should define a default value
- [warning] ddl.column.not_null.require: column "name" should be declared NOT NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing table comment, missing audit columns (created_at / updated_at), missing column comments, missing default values, name allows NULL. A "working" CREATE TABLE has 8 compliance issues — every column added later compounds the debt.&lt;/p&gt;

&lt;p&gt;Corrected version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="nb"&gt;unsigned&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'primary key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'display name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'created at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt; &lt;span class="s1"&gt;'updated at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;InnoDB&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;CHARSET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;utf8mb4&lt;/span&gt; &lt;span class="k"&gt;COMMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'demo table'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scenario 6: Three PostgreSQL Migration Pitfalls
&lt;/h2&gt;

&lt;p&gt;PostgreSQL has a different DDL locking model than MySQL. Some risks are PG-specific. Take this migration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--dialect&lt;/span&gt; postgresql &lt;span class="nt"&gt;--file&lt;/span&gt; pg_migration.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: reject
- Statements: 3 | Blockers: 1 | Warnings: 4

## Statement 1: ALTER TABLE orders ALTER COLUMN amount TYPE TEXT
- [warning] ddl.alter.set_data_type.forbid: ALTER TABLE set data type is
  forbidden for "amount"
- [warning] ddl.pg.alter.set_data_type.rewrite.warn: ALTER COLUMN "amount"
  SET DATA TYPE carries table rewrite risk on PostgreSQL
  Suggestion: Assess table size and lock impact first. For large tables,
  use a phased migration: add a shadow column with the new type, backfill
  in batches, switch application reads, then drop the old column.

## Statement 2: ALTER TABLE orders ALTER COLUMN amount DROP NOT NULL
- [blocker] ddl.alter.drop_not_null.explicit_nullability_change.forbid:
  ALTER TABLE drop not null explicitly changes nullability for "amount"
- [warning] ddl.alter.drop_not_null.forbid: ALTER TABLE drop not null is
  forbidden for "amount"

## Statement 3: CREATE INDEX idx_status ON orders (status)
- [warning] ddl.pg.create_index.concurrently.require: CREATE INDEX "idx_status"
  without CONCURRENTLY can block writes on PostgreSQL
  Suggestion: Use CREATE INDEX CONCURRENTLY to build the index without
  blocking writes. Note that CONCURRENTLY cannot run inside a transaction;
  run it as a standalone migration step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three issues:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statement 1: Type change triggers table rewrite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SET DATA TYPE&lt;/code&gt; in PostgreSQL acquires an ACCESS EXCLUSIVE lock (blocks all reads and writes) and rewrites the entire table. DeltaScope suggests a phased migration: add a shadow column with the new type, backfill in batches, switch application reads, then drop the old column. This is a PG-only rule (&lt;code&gt;ddl.pg.alter.set_data_type.rewrite.warn&lt;/code&gt;) — it won't fire under the MySQL dialect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statement 2: Dropping NOT NULL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same risk as MySQL — application code depends on the NOT NULL constraint. But PG syntax uses &lt;code&gt;ALTER COLUMN ... DROP NOT NULL&lt;/code&gt; instead of MySQL's &lt;code&gt;MODIFY COLUMN&lt;/code&gt;. DeltaScope recognizes PG syntax and fires the corresponding rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Statement 3: CREATE INDEX without CONCURRENTLY&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a PG-specific pitfall. A regular &lt;code&gt;CREATE INDEX&lt;/code&gt; holds a write-blocking lock for the entire index build. &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; builds the index without blocking writes, but it can't run inside a transaction. DeltaScope checks for this and reminds you that CONCURRENTLY must be a standalone migration step.&lt;/p&gt;

&lt;p&gt;One more PG-specific scenario — adding a CHECK constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;chk_amount&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--dialect&lt;/span&gt; postgresql &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE orders ADD CONSTRAINT chk_amount CHECK (amount &amp;gt; 0)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: review

- [warning] ddl.pg.alter.add_check.not_valid.require: CHECK constraint
  "chk_amount" without NOT VALID validates all existing rows immediately
  on PostgreSQL
  Suggestion: Use a two-step approach:
  1) ADD CONSTRAINT ... NOT VALID to register the constraint without
     scanning existing rows.
  2) VALIDATE CONSTRAINT in a separate step — it holds only a SHARE
     UPDATE EXCLUSIVE lock.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, PG scans the entire table to validate the constraint at &lt;code&gt;ADD CONSTRAINT&lt;/code&gt; time, holding an ACCESS EXCLUSIVE lock on large tables. The correct approach: first register the constraint as &lt;code&gt;NOT VALID&lt;/code&gt; (no scan), then run &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; separately (only holds SHARE UPDATE EXCLUSIVE lock, doesn't block reads or writes).&lt;/p&gt;

&lt;p&gt;These rules (&lt;code&gt;ddl.pg.alter.set_data_type.rewrite.warn&lt;/code&gt;, &lt;code&gt;ddl.pg.create_index.concurrently.require&lt;/code&gt;, &lt;code&gt;ddl.pg.alter.add_check.not_valid.require&lt;/code&gt;) are exclusive to the PostgreSQL dialect. Switch with &lt;code&gt;--dialect postgresql&lt;/code&gt; — they won't fire under MySQL or TiDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 7: Dialect Differences — MySQL vs TiDB
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MySQL default audit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TiDB dialect audit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;deltascope audit &lt;span class="nt"&gt;--dialect&lt;/span&gt; tidb &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verdict: pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both pass for a single statement. But if a migration file has three ALTER statements on the same table, the MySQL dialect fires a warning (suggesting merge), while the TiDB dialect does not (because TiDB DDL is online — no table locking, no need to merge). This is what &lt;code&gt;--dialect&lt;/code&gt; is for — different engines have different best practices, and audit rules should follow the engine.&lt;/p&gt;

&lt;p&gt;Three dialects are currently supported: &lt;code&gt;mysql&lt;/code&gt; (default), &lt;code&gt;tidb&lt;/code&gt;, &lt;code&gt;postgresql&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI Integration
&lt;/h2&gt;

&lt;p&gt;All of the above checks belong in CI, not in manual review. GitHub Actions config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SQL Audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;migrations/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install DeltaScope&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -fsSL https://raw.githubusercontent.com/Fanduzi/DeltaScope/main/install.sh | sh&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deltascope audit --file ./migrations/ --format github-actions --fail-on warning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three CI output formats:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--format github-actions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PR annotations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab Code Quality&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--format gitlab-codequality&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code Quality artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SARIF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--format sarif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GitHub Code Scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For more accurate audits using live table structure (e.g., checking for redundant indexes), use metadata-aware mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE orders ADD INDEX idx_status (status)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;--port&lt;/span&gt; 3306 &lt;span class="nt"&gt;--user&lt;/span&gt; root &lt;span class="nt"&gt;--ask-password&lt;/span&gt; &lt;span class="nt"&gt;--schema&lt;/span&gt; app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This connects to the database to read table statistics but &lt;strong&gt;never executes any DDL or DML&lt;/strong&gt; — read-only metadata access.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON Output
&lt;/h2&gt;

&lt;p&gt;For CI scripts that need machine-readable results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deltascope audit &lt;span class="nt"&gt;--sql&lt;/span&gt; &lt;span class="s2"&gt;"ALTER TABLE orders MODIFY COLUMN amount VARCHAR(100) NOT NULL"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"statements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"blockers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ddl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"raw_sql"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALTER TABLE orders MODIFY COLUMN amount VARCHAR(100) NOT NULL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"findings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"rule_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ddl.alter.modify_column.explicit_nullability_change.forbid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blocker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALTER TABLE modify column explicitly changes nullability for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep nullability unchanged for &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;amount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; or relax the policy intentionally after review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modify_column"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"change_kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explicit_nullability_change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"column_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"offline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dialect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mysql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dialect_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Script logic: &lt;code&gt;verdict == "reject"&lt;/code&gt; → block, &lt;code&gt;"review"&lt;/code&gt; → require human acknowledgment, &lt;code&gt;"pass"&lt;/code&gt; → allow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule Configuration
&lt;/h2&gt;

&lt;p&gt;151 built-in rules (run &lt;code&gt;deltascope rules&lt;/code&gt; to list all), configurable via YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# deltascope.yaml&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Require DBA sign-off for DROP COLUMN&lt;/span&gt;
  &lt;span class="na"&gt;ddl.alter.drop_column.forbid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blocker&lt;/span&gt;

  &lt;span class="c1"&gt;# Enforce idx_ prefix on secondary indexes&lt;/span&gt;
  &lt;span class="na"&gt;ddl.alter.add_index.secondary.prefix.require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warning&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;idx_&lt;/span&gt;

  &lt;span class="c1"&gt;# Teams that don't need audit columns can disable&lt;/span&gt;
  &lt;span class="na"&gt;ddl.table.audit_columns.require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the config to the repo — CI loads it automatically.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://deltascope.pages.dev" rel="noopener noreferrer"&gt;https://deltascope.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/Fanduzi/DeltaScope" rel="noopener noreferrer"&gt;https://github.com/Fanduzi/DeltaScope&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rule reference: &lt;a href="https://github.com/Fanduzi/DeltaScope/blob/main/configs/deltascope.example.yaml" rel="noopener noreferrer"&gt;https://github.com/Fanduzi/DeltaScope/blob/main/configs/deltascope.example.yaml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CI integration docs: &lt;a href="https://github.com/Fanduzi/DeltaScope/tree/main/docs/recipe" rel="noopener noreferrer"&gt;https://github.com/Fanduzi/DeltaScope/tree/main/docs/recipe&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew tap Fanduzi/deltascope &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; deltascope

&lt;span class="c"&gt;# Linux&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Fanduzi/DeltaScope/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>cli</category>
      <category>database</category>
      <category>showdev</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
