<?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: Miłosz Kordziński</title>
    <description>The latest articles on DEV Community by Miłosz Kordziński (@milekv).</description>
    <link>https://dev.to/milekv</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%2F4076003%2Ff7719960-3a5a-415a-a2d1-79a454e86c8e.jpg</url>
      <title>DEV Community: Miłosz Kordziński</title>
      <link>https://dev.to/milekv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/milekv"/>
    <language>en</language>
    <item>
      <title>I tested a PostgreSQL migration checker on 760 real migration files</title>
      <dc:creator>Miłosz Kordziński</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:06:49 +0000</pubDate>
      <link>https://dev.to/milekv/i-tested-a-postgresql-migration-checker-on-760-real-migration-files-4d9h</link>
      <guid>https://dev.to/milekv/i-tested-a-postgresql-migration-checker-on-760-real-migration-files-4d9h</guid>
      <description>&lt;p&gt;A migration can be valid SQL and still be a bad production change.&lt;/p&gt;

&lt;p&gt;An index build can block writes. A type change can rewrite a large table. A foreign key can validate every existing row at deployment time. These problems are easy to miss in review because the migration often looks harmless.&lt;/p&gt;

&lt;p&gt;I wanted a check that runs before PostgreSQL sees the migration, so I tested the first version of &lt;a href="https://github.com/milekv/pg-migration-guard" rel="noopener noreferrer"&gt;pg-migration-guard&lt;/a&gt; against two established open source codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test set
&lt;/h2&gt;

&lt;p&gt;I used the migration histories from Cal.com and Documenso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;760 SQL migration files&lt;/li&gt;
&lt;li&gt;2,638 SQL statements&lt;/li&gt;
&lt;li&gt;1,340 findings from the initial full-history scan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those 1,340 findings are not 1,340 confirmed bugs. They are rule matches that need context. That distinction matters.&lt;/p&gt;

&lt;p&gt;The first implementation could detect useful patterns, but running it over an entire migration history on every pull request was the wrong workflow. Old migrations cannot be changed safely in many projects, and repeating old warnings makes new warnings easier to ignore.&lt;/p&gt;

&lt;p&gt;The useful unit of review is usually the migration introduced by the current change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking only the current change
&lt;/h2&gt;

&lt;p&gt;The CLI now accepts a Git reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx pg-migration-guard &lt;span class="s2"&gt;"migrations/**/*.sql"&lt;/span&gt; &lt;span class="nt"&gt;--changed-since&lt;/span&gt; origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It intersects the supplied paths with SQL files that were added, copied, modified, or renamed in &lt;code&gt;origin/main...HEAD&lt;/code&gt;. Existing migration history stays out of the result.&lt;/p&gt;

&lt;p&gt;In GitHub Actions the check can be kept small:&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;Migration safety&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrations/**/*.sql"&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;check&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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npx --yes pg-migration-guard@0.2.0 "migrations/**/*.sql" --changed-since origin/${{ github.base_ref }} --format github&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GitHub output format creates annotations on the relevant lines. The command also has separate exit codes for a failed safety threshold and for invalid input or configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;p&gt;The current rules cover a deliberately limited set of PostgreSQL operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;indexes created without &lt;code&gt;CONCURRENTLY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; inside a transaction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SET NOT NULL&lt;/code&gt; on existing tables&lt;/li&gt;
&lt;li&gt;column type changes that may rewrite a table&lt;/li&gt;
&lt;li&gt;volatile defaults&lt;/li&gt;
&lt;li&gt;foreign keys that validate existing rows immediately&lt;/li&gt;
&lt;li&gt;table and column removal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALTER TABLE&lt;/code&gt; without a migration-level &lt;code&gt;lock_timeout&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default output includes the lock level when it is relevant and suggests a safer deployment sequence.&lt;/p&gt;

&lt;p&gt;For example, a direct &lt;code&gt;SET NOT NULL&lt;/code&gt; can be replaced with a &lt;code&gt;CHECK (column IS NOT NULL) NOT VALID&lt;/code&gt;, validation in a separate step, and then the final constraint change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database context is optional
&lt;/h2&gt;

&lt;p&gt;A static check cannot know whether a table has ten rows or ten million. The CLI can connect to PostgreSQL using a restricted role and read catalog metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx pg-migration-guard &lt;span class="s2"&gt;"migrations/**/*.sql"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--database-url&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection is used only for catalog inspection inside a read-only transaction. Migration SQL is never sent to the database. Table size and estimated row count can then raise the severity of a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries
&lt;/h2&gt;

&lt;p&gt;This is not a PostgreSQL parser and it does not prove that a migration is safe. The checks are deterministic and intentionally narrow. Dynamic SQL, application deployment order, and production rehearsal are outside the current scope.&lt;/p&gt;

&lt;p&gt;That limitation is useful. A small check that explains exactly why it matched is easier to trust than a broad score with unclear reasoning.&lt;/p&gt;

&lt;p&gt;The package is available on &lt;a href="https://www.npmjs.com/package/pg-migration-guard" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, and the source, rules, examples, and database queries are in the &lt;a href="https://github.com/milekv/pg-migration-guard" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next work is driven by reproducible false positives and migration layouts that the current file selection does not handle well.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Running the same SQL checks in a browser, CLI and pull request</title>
      <dc:creator>Miłosz Kordziński</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:32:30 +0000</pubDate>
      <link>https://dev.to/milekv/running-the-same-sql-checks-in-a-browser-cli-and-pull-request-52hk</link>
      <guid>https://dev.to/milekv/running-the-same-sql-checks-in-a-browser-cli-and-pull-request-52hk</guid>
      <description>&lt;p&gt;I wanted one set of SQL checks to work in three places: while exploring a query, from a terminal and during code review.&lt;/p&gt;

&lt;p&gt;That became SQL Atlas. It is a local, deterministic SQL analyzer with a browser interface, a CLI and a GitHub Action. This article covers the interfaces, the CI contract and the limits of static SQL analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  One analyzer, three interfaces
&lt;/h2&gt;

&lt;p&gt;The analyzer returns structured data instead of printing messages directly. Each interface decides how to present the same result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser explains findings and links them to learning material.&lt;/li&gt;
&lt;li&gt;The CLI returns text, JSON or Markdown and uses stable exit codes.&lt;/li&gt;
&lt;li&gt;The GitHub Action converts findings into file annotations and a job summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping presentation outside the analyzer prevents the CLI and Action from becoming separate implementations with different behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CLI needs a contract
&lt;/h2&gt;

&lt;p&gt;The CLI accepts one or more files, or SQL through standard input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; sql-atlas@0.5.1 analyze query.sql
&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SELECT * FROM customers;"&lt;/span&gt; | npx &lt;span class="nt"&gt;--yes&lt;/span&gt; sql-atlas@0.5.1 analyze -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It supports PostgreSQL, MySQL, Oracle, SQLite, SQL Server and a generic mode. Output can be text for a person, JSON for another program or Markdown for an issue or report.&lt;/p&gt;

&lt;p&gt;Exit codes are part of the interface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; means analysis completed and the configured policy passed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt; means analysis completed but a severity or score threshold failed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt; means the command or input was invalid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction matters in CI. A policy failure is not the same as a broken invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning findings into pull request feedback
&lt;/h2&gt;

&lt;p&gt;The Action runs as a bundled Node 24 program and does not download dependencies at runtime. A minimal workflow looks like this:&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 review&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.sql"&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&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;sql-atlas&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@v7&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;milekv/sql-atlas@v0.5.1&lt;/span&gt;
        &lt;span class="na"&gt;with&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="s"&gt;migrations/**/*.sql&lt;/span&gt;
            &lt;span class="s"&gt;schema/**/*.sql&lt;/span&gt;
          &lt;span class="na"&gt;dialect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql&lt;/span&gt;
          &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;
          &lt;span class="na"&gt;min-score&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings become GitHub file annotations. The full result is written to the job summary, and the Action exposes file count, finding count and lowest score as outputs.&lt;/p&gt;

&lt;p&gt;The default policy only fails on critical findings. Teams can start in report-only mode with &lt;code&gt;fail-on: none&lt;/code&gt;, inspect false positives and add stricter thresholds later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What static analysis cannot know
&lt;/h2&gt;

&lt;p&gt;SQL Atlas does not connect to a database. It cannot know table sizes, data distribution, available indexes, planner settings or the real execution plan.&lt;/p&gt;

&lt;p&gt;For that reason, a warning such as a function applied to a filtered column means "check whether this blocks the index strategy you expect", not "this query is slow". Runtime performance still needs &lt;code&gt;EXPLAIN&lt;/code&gt;, representative data and production-like measurements.&lt;/p&gt;

&lt;p&gt;The browser includes a local PostgreSQL EXPLAIN JSON viewer for that next step, but the analyzer deliberately keeps its claims narrow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the distribution surfaces
&lt;/h2&gt;

&lt;p&gt;The project tests the analyzer and both automation interfaces. CI builds the web app, CLI and Action bundle. A smoke workflow runs the repository's own Action against a known SQL file and verifies its outputs. CI also rebuilds the committed Action bundle and checks that it has no uncommitted difference.&lt;/p&gt;

&lt;p&gt;The CLI package has no runtime dependencies. I verified the public npm package from an empty directory with a clean cache, including the executable version and a real stdin analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://milekv.github.io/sql-atlas/" rel="noopener noreferrer"&gt;https://milekv.github.io/sql-atlas/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/milekv/sql-atlas" rel="noopener noreferrer"&gt;https://github.com/milekv/sql-atlas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/sql-atlas" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/sql-atlas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;v0.5.1: &lt;a href="https://github.com/milekv/sql-atlas/releases/tag/v0.5.1" rel="noopener noreferrer"&gt;https://github.com/milekv/sql-atlas/releases/tag/v0.5.1&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am particularly interested in examples where a rule is too broad, misses a dialect detail or produces an unhelpful CI annotation.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>github</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
