<?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: Feroz Shah</title>
    <description>The latest articles on DEV Community by Feroz Shah (@fsmutimeer).</description>
    <link>https://dev.to/fsmutimeer</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%2F198187%2F087f6ad2-b7f9-427c-8188-e7f594b4aaf7.jpg</url>
      <title>DEV Community: Feroz Shah</title>
      <link>https://dev.to/fsmutimeer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fsmutimeer"/>
    <language>en</language>
    <item>
      <title>Spring Boot: migrations at startup, health checks and metrics</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sun, 08 Nov 2026 06:23:23 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/spring-boot-migrations-at-startup-health-checks-and-metrics-1mng</link>
      <guid>https://dev.to/fsmutimeer/spring-boot-migrations-at-startup-health-checks-and-metrics-1mng</guid>
      <description>&lt;p&gt;Migrations are written in development, but they have to run wherever the application runs: on a colleague's laptop, in CI, in staging, in production. The simplest way to get there is to let the application apply them itself, each time it starts, before anything else touches the database.&lt;/p&gt;

&lt;p&gt;This article adds the Migrax starter to a Spring Boot application, follows what happens at startup, starts two instances at once, and looks at what Actuator shows: a health check and Prometheus metrics. At the end, the pipeline runs the migrations instead of the application.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.1, Spring Boot 3.5.7, Hibernate 6.6.33, Micrometer 1.15.5, PostgreSQL 17. Every output below was copied from a real run; log lines are shortened to the message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A shop with two entities, &lt;code&gt;Customer&lt;/code&gt; and &lt;code&gt;PurchaseOrder&lt;/code&gt;. Besides the usual web and JPA starters, the application has Actuator, the Prometheus registry and the Migrax starter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-actuator&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.micrometer&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;micrometer-registry-prometheus&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.migrax&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;migrax-spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.17.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;application.properties&lt;/code&gt; has the database, Hibernate set to only check the schema, and the two Actuator endpoints used below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.datasource.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jdbc:postgresql://localhost:5432/shop&lt;/span&gt;
&lt;span class="py"&gt;spring.datasource.username&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="py"&gt;spring.datasource.password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
&lt;span class="py"&gt;spring.jpa.hibernate.ddl-auto&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;validate&lt;/span&gt;

&lt;span class="py"&gt;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;health,prometheus&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.health.show-details&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migrations come from the entities. &lt;code&gt;migrax init&lt;/code&gt; sets up the project (it reads the database settings from &lt;code&gt;application.properties&lt;/code&gt;), and &lt;code&gt;generate&lt;/code&gt; writes the first migration:&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;migrax generate
No snapshot yet: using the current database schema as the baseline.
Created src/main/resources/db/migration/0001_initial.sql:
  - create table customers
  - create table purchase_orders
  - add foreign key fk_purchase_orders_cu_1ba47d9c on purchase_orders
Rollback script written to src/main/resources/db/migration/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file sits in &lt;code&gt;src/main/resources/db/migration&lt;/code&gt;, so it is packaged into the jar. That is where the starter looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens at startup
&lt;/h2&gt;

&lt;p&gt;Start the application on the empty database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HikariPool-1 - Start completed.
Migrax: Applying migration '0001_initial.sql'...
Migrax: Successfully applied migration '0001_initial.sql'.
Migrax: applied 1 migration(s)
Tomcat started on port 8080 (http) with context path '/'
Started ShopApplication in 9.05 seconds (process running for 10.302)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters. The starter runs as soon as the connection pool is there and &lt;strong&gt;before&lt;/strong&gt; JPA starts, so when Hibernate validates the schema, the tables already exist. With &lt;code&gt;ddl-auto=validate&lt;/code&gt;, a database that doesn't match the entities stops the start, instead of failing on the first request.&lt;/p&gt;

&lt;p&gt;Start it again and there is nothing to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrax: schema is up to date
Started ShopApplication in 8.571 seconds (process running for 9.58)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application never needs the &lt;code&gt;migrax&lt;/code&gt; command. It reads the migration files from the jar and records each one in the history table in the database, with a checksum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Several instances starting together
&lt;/h2&gt;

&lt;p&gt;A new field on &lt;code&gt;Customer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@ColumnDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"'GB'"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GB"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrax generate&lt;/code&gt; writes &lt;code&gt;0002_add_customers_country.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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;country&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;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'GB'&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;In production, the new version rarely starts alone: two or more instances start at the same moment. Only one of them may apply the migration. Migrax takes a lock in the database first (on PostgreSQL an advisory lock), and by default an instance that doesn't get it stops at once. Here are the two instances, started together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- instance 1
Migrax: Applying migration '0002_add_customers_country.sql'...
Migrax: Successfully applied migration '0002_add_customers_country.sql'.
Migrax: applied 1 migration(s)
Started ShopApplication in 10.515 seconds (process running for 11.809)

--- instance 2
Caused by: org.migrax.dialect.MigrationLockHeldException: Another Migrax process is applying migrations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is broken: the migration ran once. But the second instance failed and has to be restarted, and an orchestrator counts that as a crash. Tell the instances to wait for each other instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;migrax.lock-timeout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the wait visible, the next run has more to do. The table now has two million customers, and an index on &lt;code&gt;full_name&lt;/code&gt; is added to the entity (&lt;code&gt;@Table(indexes = @Index(columnList = "fullName"))&lt;/code&gt;). &lt;code&gt;generate&lt;/code&gt; writes it as &lt;code&gt;0003_index_customers.sql&lt;/code&gt;, and warns about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/main/resources/db/migration/0003_index_customers.sql, statement 1: warning MX001
  CREATE INDEX idx_customers_full_name ON customers (full_name)
  Building this index blocks writes to customers until it finishes.
  Fix: Use CREATE INDEX CONCURRENTLY in its own migration that starts with '-- migrax:no-transaction' ('migrax generate --safe' does this).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth knowing on a busy table. Here it is fine. Both instances start again, with &lt;code&gt;0002&lt;/code&gt; and &lt;code&gt;0003&lt;/code&gt; pending (&lt;code&gt;0002&lt;/code&gt; was rolled back to run again):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- instance 1
Migrax: Applying migration '0002_add_customers_country.sql'...
Migrax: Successfully applied migration '0002_add_customers_country.sql'.
Migrax: Applying migration '0003_index_customers.sql'...
Migrax: Successfully applied migration '0003_index_customers.sql'.
Migrax: applied 2 migration(s)
Started ShopApplication in 17.185 seconds (process running for 18.461)

--- instance 2
Migrax: Another Migrax process holds the migration lock; waiting up to 2m...
Migrax: schema is up to date
Started ShopApplication in 18.851 seconds (process running for 20.138)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second instance waited until the first had built the index, found nothing left to do, and started. Set the timeout longer than your slowest migration takes; when it runs out, the instance stops as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The health check
&lt;/h2&gt;

&lt;p&gt;With Actuator in the application, the starter adds a &lt;code&gt;migrax&lt;/code&gt; health component:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/actuator/health/migrax
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;:&lt;span class="s2"&gt;"UP"&lt;/span&gt;,&lt;span class="s2"&gt;"details"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"applied"&lt;/span&gt;:3,&lt;span class="s2"&gt;"pending"&lt;/span&gt;:0,&lt;span class="s2"&gt;"problems"&lt;/span&gt;:0&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;problems&lt;/code&gt; counts migrations that failed, were edited after they were applied, or are in the history but no longer in the jar. Any of those, or a pending migration, makes the component DOWN. Turn it off with &lt;code&gt;management.health.migrax.enabled=false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An edited migration is the one people run into. Someone changes &lt;code&gt;varchar(20)&lt;/code&gt; to &lt;code&gt;varchar(30)&lt;/code&gt; in a migration that production has already applied, and the next start stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'migraxMigrator' defined in class path resource [org/migrax/spring/MigraxAutoConfiguration.class]: Checksum changed for applied migration 0004_add_purchase_orders_status.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database already has the old version of that migration, and it never gets the new one. Leave applied migrations alone; a change goes into a new migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics
&lt;/h2&gt;

&lt;p&gt;The application has a Micrometer registry (here Prometheus), so Migrax records its migrations there without any setting. After the run above, the first instance shows:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/actuator/prometheus | &lt;span class="nb"&gt;grep&lt;/span&gt; ^migrax
migrax_lock_wait_seconds_max&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 0.044737607
migrax_migration_seconds_max&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;,migration&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0002_add_customers_country.sql"&lt;/span&gt;,outcome&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 0.005012614
migrax_migration_seconds_max&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;,migration&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0003_index_customers.sql"&lt;/span&gt;,outcome&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 6.10576425
migrax_migrations_applied_total&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 2.0
migrax_migrations_pending&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 0.0
migrax_run_seconds_max&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;,outcome&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 6.212559502
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Only the &lt;code&gt;_max&lt;/code&gt; lines of each timer are shown; &lt;code&gt;_count&lt;/code&gt; and &lt;code&gt;_sum&lt;/code&gt; are there too.) The second instance, the one that waited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrax_lock_wait_seconds_max{database="default"} 7.803599713
migrax_run_seconds_max{database="default",outcome="success"} 7.843474718
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That answers the questions that come up after a slow deployment: which migration took the time (the index, 6.1 seconds), and how long the other instances waited for it (7.8 seconds). For an alert, the counter of failed migrations is the one to watch:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MigraxMigrationFailed&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;increase(migrax_migrations_failed_total[15m]) &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With an &lt;code&gt;OpenTelemetry&lt;/code&gt; bean, each run and each migration also becomes a span in the trace of the startup. &lt;code&gt;migrax.metrics.enabled=false&lt;/code&gt; and &lt;code&gt;migrax.tracing.enabled=false&lt;/code&gt; turn them off.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a pipeline migrates
&lt;/h2&gt;

&lt;p&gt;Some teams don't let the application change the schema. A deployment pipeline or a DBA runs the migrations, and the application only reads. One setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;migrax.migrate-at-startup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order then becomes the pipeline's job. A new &lt;code&gt;status&lt;/code&gt; column on &lt;code&gt;PurchaseOrder&lt;/code&gt;, deployed before the pipeline has migrated, doesn't get far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrax: not migrating at startup (migrax.migrate-at-startup=false); 1 migration(s) pending
Caused by: org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [status] in table [purchase_orders]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;code&gt;ddl-auto=validate&lt;/code&gt; doing its job. So: run &lt;code&gt;migrax migrate&lt;/code&gt; (or the Maven goal &lt;code&gt;migrax:migrate&lt;/code&gt;) as a step before the new version starts:&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0004_add_purchase_orders_status.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0004_add_purchase_orders_status.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 4 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every pending migration stops the start. An index isn't checked by Hibernate's validation, so a release with a forgotten index migration (&lt;code&gt;0005&lt;/code&gt;) starts fine, and is slow. That is where the health check and the pending gauge come in, because they keep working when the application doesn't migrate:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;' HTTP %{http_code}\n'&lt;/span&gt; localhost:8080/actuator/health/migrax
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;:&lt;span class="s2"&gt;"DOWN"&lt;/span&gt;,&lt;span class="s2"&gt;"details"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"applied"&lt;/span&gt;:4,&lt;span class="s2"&gt;"pending"&lt;/span&gt;:1,&lt;span class="s2"&gt;"problems"&lt;/span&gt;:0&lt;span class="o"&gt;}}&lt;/span&gt; HTTP 503

&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/actuator/prometheus | &lt;span class="nb"&gt;grep&lt;/span&gt; ^migrax
migrax_migrations_pending&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt; 1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An alert on the gauge catches a pipeline step that was skipped:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MigraxMigrationsPending&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrax_migrations_pending &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep traffic away from such an instance on Kubernetes, add the component to the readiness group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;management.endpoint.health.probes.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;management.endpoint.health.group.readiness.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;readinessState,migrax&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the pipeline has run &lt;code&gt;migrax migrate&lt;/code&gt;, the application notices on its own. It asks the database at most every 10 seconds, so no restart is needed:&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;' HTTP %{http_code}\n'&lt;/span&gt; localhost:8080/actuator/health/migrax
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;:&lt;span class="s2"&gt;"UP"&lt;/span&gt;,&lt;span class="s2"&gt;"details"&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"applied"&lt;/span&gt;:5,&lt;span class="s2"&gt;"pending"&lt;/span&gt;:0,&lt;span class="s2"&gt;"problems"&lt;/span&gt;:0&lt;span class="o"&gt;}}&lt;/span&gt; HTTP 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;migrax-spring-boot-starter&lt;/code&gt;, keep the migrations in &lt;code&gt;src/main/resources/db/migration&lt;/code&gt;, and the application applies them each time it starts, before JPA, so &lt;code&gt;ddl-auto=validate&lt;/code&gt; checks the migrated schema.&lt;/li&gt;
&lt;li&gt;Several instances: set &lt;code&gt;migrax.lock-timeout&lt;/code&gt; (for example &lt;code&gt;2m&lt;/code&gt;). One instance migrates, the others wait and then start.&lt;/li&gt;
&lt;li&gt;With Actuator, the &lt;code&gt;migrax&lt;/code&gt; health component is DOWN for a pending, failed, edited or missing migration. With Micrometer, metrics show how long each migration took and how long the instances waited.&lt;/li&gt;
&lt;li&gt;When a pipeline migrates, set &lt;code&gt;migrax.migrate-at-startup=false&lt;/code&gt;, run &lt;code&gt;migrax migrate&lt;/code&gt; before the new version starts, and alert on &lt;code&gt;migrax_migrations_pending&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More in the docs: &lt;a href="https://migrax.org/frameworks/spring-boot/" rel="noopener noreferrer"&gt;Spring Boot&lt;/a&gt;, &lt;a href="https://migrax.org/guides/observability/" rel="noopener noreferrer"&gt;Metrics and tracing&lt;/a&gt; and &lt;a href="https://migrax.org/guides/containers-and-kubernetes/" rel="noopener noreferrer"&gt;Containers and Kubernetes&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/spring-boot-migrations-at-startup/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>postgres</category>
      <category>devops</category>
    </item>
    <item>
      <title>Generate and apply database migrations without leaving IntelliJ IDEA</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Fri, 06 Nov 2026 16:39:56 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/generate-and-apply-database-migrations-without-leaving-intellij-idea-5bd0</link>
      <guid>https://dev.to/fsmutimeer/generate-and-apply-database-migrations-without-leaving-intellij-idea-5bd0</guid>
      <description>&lt;p&gt;You rename a field on a JPA entity. Then you leave the editor to write the SQL, write the rollback script, check which database you are about to change, and run it. The Migrax plugin for IntelliJ IDEA keeps all of that in the IDE. This article follows one renamed field from the edit to the applied migration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.19.0-rc.2, Migrax plugin 0.1.1, IntelliJ IDEA 2026.2.1, Hibernate 6.6.13, PostgreSQL 17. Every screenshot and output below comes from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/08-overview.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxr4xf0947h6y0eglw0sn.png" alt="IntelliJ IDEA with the project tree, a migration and its rollback script, and the Migrax tool window" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Migrax plugin after a rename: the generated migration and its rollback script, and the tool window with every migration applied.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the plugin
&lt;/h2&gt;

&lt;p&gt;In IntelliJ IDEA open &lt;strong&gt;Settings &amp;gt; Plugins &amp;gt; Marketplace&lt;/strong&gt;, search for &lt;strong&gt;Migrax&lt;/strong&gt; and click &lt;strong&gt;Install&lt;/strong&gt;. The plugin is free and works in IntelliJ IDEA Community and Ultimate 2024.2 or newer.&lt;/p&gt;

&lt;p&gt;It needs the &lt;code&gt;migrax&lt;/code&gt; command. It uses the path in &lt;strong&gt;Settings &amp;gt; Tools &amp;gt; Migrax&lt;/strong&gt; if you set one, then &lt;code&gt;migrax&lt;/code&gt; on your &lt;code&gt;PATH&lt;/code&gt;, then a copy it downloaded itself. If it finds none, it offers to download the newest release (about 1 MB), checks it against its SHA-256 and unpacks it into the IDE's own folder. A Migrax you installed yourself is never changed.&lt;/p&gt;

&lt;p&gt;In a project that does not use Migrax yet, choose &lt;strong&gt;Tools &amp;gt; Migrax &amp;gt; Set Up Migrax...&lt;/strong&gt;. It runs &lt;code&gt;migrax init&lt;/code&gt; in the folder you pick and shows what Migrax detected.&lt;/p&gt;

&lt;p&gt;The margin icon in the next section needs Migrax 0.18 or newer, which is a release candidate at the time of writing. The rest of the plugin works with Migrax 0.17.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we start
&lt;/h2&gt;

&lt;p&gt;This picks up where &lt;a href="https://migrax.org/blog/renaming-columns-and-tables-without-losing-data/" rel="noopener noreferrer"&gt;Renaming columns and tables without losing data&lt;/a&gt; started: the same &lt;code&gt;customers&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt; project, two migrations applied, and two customers in the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      email       | id |     name     |      phone
------------------+----+--------------+------------------
 ada@example.com  |  1 | Ada Lovelace | +44 20 7946 0001
 alan@example.com |  2 | Alan Turing  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rename the field
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;Customer&lt;/code&gt;, rename &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt; and give the column its new name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"full_name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fullName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few seconds after the file changes, a Migrax icon appears in the margin next to the &lt;code&gt;Customer&lt;/code&gt; class, and the status bar changes from "Migrax: in sync" to "Migrax: 2 changes". Hover over the icon to see what Migrax found:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/01-entity-margin-icon.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxtdtqzniae36cptaips.png" alt="Tooltip on the margin icon: 2 changes not in a migration yet, add column customers.full_name, drop column customers.name" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two changes: a new column and a dropped one. Migrax does not know yet that they are the same column.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click the icon for &lt;strong&gt;Generate Migration&lt;/strong&gt;, or for &lt;strong&gt;Show Plan&lt;/strong&gt; to read the SQL first. They do what &lt;code&gt;migrax generate&lt;/code&gt; and &lt;code&gt;migrax plan&lt;/code&gt; do in a terminal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/02-margin-icon-menu.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bnqnwoq18ab1odi39vh.png" alt="The Migrax menu from the margin icon with Generate Migration and Show Plan" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Answer the rename question
&lt;/h2&gt;

&lt;p&gt;Generate Migration asks the question the command line asks:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/03-rename-question.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figpbqiobipweouuqkjrm.png" alt="Dialog: Did you rename customers.name to customers.full_name? with the buttons Renamed and Not Renamed" width="590" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Answer &lt;strong&gt;Renamed&lt;/strong&gt; and the migration keeps the data. Answer &lt;strong&gt;Not Renamed&lt;/strong&gt; and Migrax drops the old column with its data and adds a new, empty one. Before it drops anything it asks again, and by default it keeps the dropped data in a backup table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the migration
&lt;/h2&gt;

&lt;p&gt;The new migration and its rollback script open in the editor, next to the earlier ones:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/04-generated-migration-files.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyxf9lwbj3cser4h1ftbq.png" alt="Project tree with migrations 0001 to 0003 and a rollback folder, and the 0003 migration above its rollback script" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/0003_rename_customers_name.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;-- Generated by Migrax 0.19.0-rc.2 for postgresql&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;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db/migration/rollback/0003_rename_customers_name.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;-- Generated by Migrax 0.19.0-rc.2 for postgresql&lt;/span&gt;
&lt;span class="c1"&gt;-- Rollback for 0003_rename_customers_name.sql. 'migrax rollback' runs it.&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;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The highlighted line in the migration is a lint finding. Hover over it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/06-lint-warning-tooltip.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnionvxgehmv5lkehuxz2.png" alt="Tooltip MX008 on the RENAME COLUMN statement: application instances still running the old code will fail, with the fix" width="797" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is the same message &lt;code&gt;migrax generate&lt;/code&gt; prints in a terminal. A rename moves no data, so the statement is fast. The warning is about application instances that still run the old code during a deploy and look for the old column name. The way out it names, &lt;code&gt;migrax generate --zero-downtime&lt;/code&gt;, is in the plugin as &lt;strong&gt;Generate for Zero Downtime&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the migration
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;Tools &amp;gt; Migrax &amp;gt; Migrate...&lt;/strong&gt;, or the Migrate button in the tool window. Before it runs anything, the plugin shows which database it will change and which migrations it will apply:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/05-migrate-confirmation.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk05vv9y0ffuf0w2uq8yf.png" alt="Dialog: Apply 1 migration to this database? jdbc:postgresql://localhost:5432/shop, 0003_rename_customers_name.sql" width="664" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Confirm, and the migration runs in a background task, so the editor stays usable. The status bar returns to "Migrax: in sync", and the &lt;strong&gt;Migrax&lt;/strong&gt; tool window lists every migration and its state. Click the status bar entry to open it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/assets/blog/intellij/07-tool-window-status.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F35v693dfgixudwosrd5c.png" alt="The Migrax tool window, Status tab: three migrations, all applied" width="800" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rename moved no data. The same two customers are in the table, under the new column name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |      email       |  full_name   |      phone
----+------------------+--------------+------------------
  1 | ada@example.com  | Ada Lovelace | +44 20 7946 0001
  2 | alan@example.com | Alan Turing  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What else is in the tool window
&lt;/h2&gt;

&lt;p&gt;The tool window has four tabs, and its buttons run the other commands. The &lt;a href="https://migrax.org/intellij/" rel="noopener noreferrer"&gt;plugin guide&lt;/a&gt; lists every one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Status tab&lt;/td&gt;
&lt;td&gt;Every migration and its state: applied, pending, failed, changed or missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan tab&lt;/td&gt;
&lt;td&gt;The SQL of the next migration, with its lint findings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint tab&lt;/td&gt;
&lt;td&gt;Locking and risky SQL in the pending migrations, or in all of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environments tab&lt;/td&gt;
&lt;td&gt;Every configured database: in sync, pending, drift or unreachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roll Back...&lt;/td&gt;
&lt;td&gt;Shows the database and the newest applied migration, then rolls it back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift&lt;/td&gt;
&lt;td&gt;Lists changes made to the database outside the migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify&lt;/td&gt;
&lt;td&gt;Applies every migration to a throwaway database and tests each rollback script, leaving your database alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report&lt;/td&gt;
&lt;td&gt;Opens an HTML report of the schema, the migrations, the lint findings and the drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doctor&lt;/td&gt;
&lt;td&gt;Checks Java, the build, the entities and the database connection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The plugin contains no migration logic. Every button runs the &lt;code&gt;migrax&lt;/code&gt; command in a background task, asks for &lt;code&gt;--json&lt;/code&gt; output and draws the answer: the tool window tabs, the margin icon and the underlines. The IDE, your terminal and your CI call the same command, so they give the same result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;With several Migrax projects below one IDE project, the first one found is used.&lt;/li&gt;
&lt;li&gt;Migrate and Roll Back work on the database in the project's settings. Other environments show in the Environments tab and are migrated from the command line.&lt;/li&gt;
&lt;li&gt;Migrate and Roll Back cannot be cancelled once they start, because stopping halfway could leave a migration half applied.&lt;/li&gt;
&lt;li&gt;The status bar and the margin icons check again about 5 seconds after an entity, migration or settings file changes. That check may build the project, and you can turn it off in &lt;strong&gt;Settings &amp;gt; Tools &amp;gt; Migrax&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Squash and merge are not in the IDE yet: use &lt;code&gt;migrax squash&lt;/code&gt; and &lt;code&gt;migrax merge&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Only IntelliJ IDEA is supported. Other JetBrains IDEs are not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The Migrax plugin takes an entity edit to an applied migration inside IntelliJ IDEA. The margin icon shows what changed, &lt;strong&gt;Generate Migration&lt;/strong&gt; writes the SQL and the rollback script and asks about renames, lint marks risky statements in the file, and &lt;strong&gt;Migrate...&lt;/strong&gt; names the database before it runs. It is an early version (0.1.1), free, and open source under the Apache License 2.0.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://plugins.jetbrains.com/plugin/35665-migrax" rel="noopener noreferrer"&gt;Install it from the JetBrains Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://migrax.org/intellij/" rel="noopener noreferrer"&gt;Read the plugin guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fsmutimeer/migrax-intellij" rel="noopener noreferrer"&gt;Browse the source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fsmutimeer/migrax-intellij/issues" rel="noopener noreferrer"&gt;Report a bug or request a feature&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/database-migrations-in-intellij-idea/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>intellij</category>
      <category>hibernate</category>
      <category>database</category>
    </item>
    <item>
      <title>Generated SQL migrations for Python, TypeScript and Go projects</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:55:26 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/generated-sql-migrations-for-python-typescript-and-go-projects-2jc2</link>
      <guid>https://dev.to/fsmutimeer/generated-sql-migrations-for-python-typescript-and-go-projects-2jc2</guid>
      <description>&lt;p&gt;Migrax started as a tool for Java: it reads JPA entities and writes the SQL migrations that bring the database in line with them. But the idea doesn't depend on Java. Every ORM knows the tables it expects, and almost every ORM can print them as &lt;code&gt;CREATE TABLE&lt;/code&gt; statements.&lt;/p&gt;

&lt;p&gt;That SQL is all Migrax needs. Keep it in a schema file, and Migrax compares it with what the existing migrations build, writes the next numbered migration with its rollback script, and applies it. This article follows a small Python project through its first changes, then shows the same loop with Drizzle in TypeScript and GORM in Go.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.2-rc.1, PostgreSQL 17, SQLAlchemy 2.1.6 (Python 3.13), Drizzle ORM 0.45.4 with drizzle-kit 0.31.11 (Node.js 24), GORM 1.31.2 (Go 1.25). Docker runs the throwaway databases. Every output below was copied from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The idea in one picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;models.py  --export--&amp;gt;  db/schema.sql  --migrax generate--&amp;gt;  db/migrations/0002_....sql
 (your ORM)              (the tables                          + rollback/0002_....sql
                          you want)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema file says what the tables should look like. The migrations say how to get there, one reviewed step at a time. Both are committed. Migrax never touches your models, and your application never needs Migrax at runtime: the migrations are plain SQL files.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Python shop with SQLAlchemy
&lt;/h2&gt;

&lt;p&gt;Two tables, customers and their orders:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DeclarativeBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapped_column&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeclarativeBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&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="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&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="n"&gt;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLAlchemy compiles a table into PostgreSQL's &lt;code&gt;CREATE TABLE&lt;/code&gt; without connecting to anything. A short script turns the models into the schema file:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export_schema.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Prints the tables of models.py as SQL, for Migrax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s schema file.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.dialects&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.schema&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CreateIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateTable&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;

&lt;span class="n"&gt;dialect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sorted_tables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;CreateIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrax reads its settings from the environment. The schema file is one more of them:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jdbc:postgresql://localhost:55432/shop
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_DATABASE_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;shop
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_DATABASE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_LOCATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;filesystem:db/migrations
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_SCHEMA_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db/schema.sql
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MIGRAX_DRIVERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;drivers        &lt;span class="c"&gt;# holds postgresql-42.7.14.jar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The first migration
&lt;/h3&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;python export_schema.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; db/schema.sql
&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
Starting a throwaway postgresql database &lt;span class="o"&gt;(&lt;/span&gt;postgres:17-alpine&lt;span class="o"&gt;)&lt;/span&gt;...
Read db/schema.sql: 2 table&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
No snapshot yet: using the current database schema as the baseline.
Created db/migrations/0001_initial.sql:
  - create table customer
  - create table orders
  - add index ix_orders_customer_id on orders
  - add foreign key orders_customer_id_fkey on orders
Rollback script written to db/migrations/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read the file, Migrax runs it on a throwaway PostgreSQL in Docker, in the same version as your server, and reads the tables back. So whatever the database accepts, Migrax understands, and what it doesn't accept fails here, not in production. The migration it writes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migrations/0001_initial.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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customer&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;integer&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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="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;150&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;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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&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;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;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;customer_pkey&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="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uk_customer_email&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;TABLE&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;id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;integer&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="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&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;span class="n"&gt;status&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;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'new'&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;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_pkey&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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;ix_orders_customer_id&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;customer_id&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;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_customer_id_fkey&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&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;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details differ from SQLAlchemy's own SQL, and both are on purpose. SQLAlchemy writes &lt;code&gt;SERIAL&lt;/code&gt;; Migrax writes identity columns, which hand out ids the same way. And constraints get names (&lt;code&gt;uk_customer_email&lt;/code&gt;), so later migrations can refer to them.&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0001_initial.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0001_initial.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 1 total &lt;span class="k"&gt;in &lt;/span&gt;db/migrations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application saves its first customer, Ada Lovelace, with one order of 42.50.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a column
&lt;/h3&gt;

&lt;p&gt;A phone number for customers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Export again. Before writing anything, &lt;code&gt;migrax check&lt;/code&gt; tells you what has no migration yet:&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;python export_schema.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; db/schema.sql
&lt;span class="nv"&gt;$ &lt;/span&gt;migrax check
Starting a throwaway postgresql database &lt;span class="o"&gt;(&lt;/span&gt;postgres:17-alpine&lt;span class="o"&gt;)&lt;/span&gt;...
Read db/schema.sql: 2 table&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
1 change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;db/schema.sql have no migration:
  - add column customer.phone
Run &lt;span class="s1"&gt;'migrax generate'&lt;/span&gt; and commit the result.
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 2 means "changes without a migration", which is the line to put in CI. Then:&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;migrax generate
Created db/migrations/0002_add_customer_phone.sql:
  - add column customer.phone
Rollback script written to db/migrations/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database started this time: Migrax had read this version of the file a moment earlier and cached what it found in &lt;code&gt;.migrax/schema-model.json&lt;/code&gt;. The migration and its rollback script:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migrations/0002_add_customer_phone.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customer&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;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db/migrations/rollback/0002_add_customer_phone.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;-- Rollback for 0002_add_customer_phone.sql. 'migrax rollback' runs it.&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: deletes data written to customer.phone since the migration ran.&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;customer&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Renaming a column, keeping the data
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;name&lt;/code&gt; becomes &lt;code&gt;full_name&lt;/code&gt;. Compared naively, that is a dropped column and a new one, and Ada's name would be gone. Migrax notices the likely rename. In a terminal it asks; in a script, where nobody can answer, it stops before dropping anything:&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;migrax generate
Possible rename: customer.name to customer.full_name. To keep the data, rerun with &lt;span class="nt"&gt;--rename&lt;/span&gt; customer.name&lt;span class="o"&gt;=&lt;/span&gt;full_name.
2 change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;postgresql:
  1. add column customer.full_name
     ALTER TABLE customer ADD COLUMN full_name varchar&lt;span class="o"&gt;(&lt;/span&gt;100&lt;span class="o"&gt;)&lt;/span&gt; NOT NULL&lt;span class="p"&gt;;&lt;/span&gt;
  2. drop column customer.name  &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE]
     ALTER TABLE customer DROP COLUMN name&lt;span class="p"&gt;;&lt;/span&gt;
error[MXE108]: 1 destructive change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; detected &lt;span class="o"&gt;(&lt;/span&gt;marked &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE] above&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Review them, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax generate --allow-destructive'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; If a column or table was renamed, pass &lt;span class="nt"&gt;--rename&lt;/span&gt; table.old&lt;span class="o"&gt;=&lt;/span&gt;new or &lt;span class="nt"&gt;--rename-table&lt;/span&gt; &lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;new instead.
&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate &lt;span class="nt"&gt;--rename&lt;/span&gt; customer.name&lt;span class="o"&gt;=&lt;/span&gt;full_name
Created db/migrations/0003_rename_customer_name.sql:
  - rename column customer.name to full_name
Rollback script written to db/migrations/rollback.
db/migrations/0003_rename_customer_name.sql, statement 1: warning MX008
  ALTER TABLE customer RENAME COLUMN name TO full_name
  Application instances still running the old code use the old name and will fail.
  Fix: Rename &lt;span class="k"&gt;in &lt;/span&gt;steps: add the new name, write to both, switch reads, &lt;span class="k"&gt;then &lt;/span&gt;remove the old name &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrax generate --zero-downtime'&lt;/span&gt; does this&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Or deploy during a maintenance window.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db/migrations/0003_rename_customer_name.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warning is about deployment, not data: old instances that still query &lt;code&gt;name&lt;/code&gt; break when the column is renamed. After &lt;code&gt;migrax migrate&lt;/code&gt;, a three-line script that lists the customers through SQLAlchemy reads the row back under its new name:&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;python read.py
1 ada@example.com Ada Lovelace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Proving the migrations
&lt;/h3&gt;

&lt;p&gt;The migrations now describe the schema in three steps. &lt;code&gt;migrax verify&lt;/code&gt; checks that they really build what the schema file says, on an empty throwaway database of your server's version, and that every rollback script works:&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;migrax verify
Starting a throwaway postgresql database &lt;span class="o"&gt;(&lt;/span&gt;postgres:17-alpine&lt;span class="o"&gt;)&lt;/span&gt;...
Applying migration &lt;span class="s1"&gt;'0001_initial.sql'&lt;/span&gt;...
...
Applied 3 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; to a fresh postgresql database.
Schema matches the schema file &lt;span class="o"&gt;(&lt;/span&gt;structural comparison&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolling back migration &lt;span class="s1"&gt;'0003_rename_customer_name.sql'&lt;/span&gt;...
...
Rollback scripts: ok &lt;span class="o"&gt;(&lt;/span&gt;3 rolled back and re-applied&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;migrax status&lt;/code&gt; shows where the real database is:&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;migrax status
Migrations &lt;span class="k"&gt;in &lt;/span&gt;db/migrations &lt;span class="k"&gt;for &lt;/span&gt;jdbc:postgresql://localhost:55432/shop
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0001_initial.sql               applied 2026-10-26 16:02:43
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0002_add_customer_phone.sql    applied 2026-10-26 16:02:51
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0003_rename_customer_name.sql  applied 2026-10-26 16:02:58
3 applied.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undoing the rename takes one command. In a terminal, &lt;code&gt;rollback&lt;/code&gt; asks before it changes anything; in a script, pass &lt;code&gt;--yes&lt;/code&gt;:&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;migrax rollback &lt;span class="nt"&gt;--yes&lt;/span&gt;
Rolling back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0003_rename_customer_name.sql
Rolling back migration &lt;span class="s1"&gt;'0003_rename_customer_name.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0003_rename_customer_name.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolled back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column is called &lt;code&gt;name&lt;/code&gt; again, with Ada's name still in it. &lt;code&gt;migrax migrate&lt;/code&gt; applies the rename once more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same loop in TypeScript, with Drizzle
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;drizzle-kit export&lt;/code&gt; prints a Drizzle schema as SQL, without a database:&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;npx drizzle-kit &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql &lt;span class="nt"&gt;--schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./schema.ts &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; db/schema.sql
&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
Starting a throwaway postgresql database &lt;span class="o"&gt;(&lt;/span&gt;postgres:17-alpine&lt;span class="o"&gt;)&lt;/span&gt;...
Read db/schema.sql: 2 table&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
No snapshot yet: using the current database schema as the baseline.
Created db/migrations/0001_initial.sql:
  - create table customer
  - create table orders
  - add index orders_customer on orders
  - add foreign key orders_customer_id_customer_id_fk on orders
Rollback script written to db/migrations/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;phone: varchar("phone", { length: 30 })&lt;/code&gt; to the &lt;code&gt;customer&lt;/code&gt; table and exporting again gives &lt;code&gt;0002_add_customer_phone.sql&lt;/code&gt; with the same single &lt;code&gt;ALTER TABLE customer ADD COLUMN&lt;/code&gt; as in Python. Drizzle's own names for indexes and foreign keys are kept as they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same loop in Go, with GORM
&lt;/h2&gt;

&lt;p&gt;GORM can't print its models as SQL, but its &lt;code&gt;AutoMigrate&lt;/code&gt; creates them. Let it create the tables in a database of their own (&lt;code&gt;shop_dev&lt;/code&gt;, never the one Migrax migrates), and copy them from there with &lt;code&gt;pg_dump&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;models.go&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;uint&lt;/span&gt;      &lt;span class="s"&gt;`gorm:"primaryKey"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`gorm:"size:150;not null;uniqueIndex"`&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`gorm:"size:100;not null"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`gorm:"not null;default:now()"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;         &lt;span class="kt"&gt;uint&lt;/span&gt;    &lt;span class="s"&gt;`gorm:"primaryKey"`&lt;/span&gt;
    &lt;span class="n"&gt;CustomerID&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;    &lt;span class="s"&gt;`gorm:"not null;index"`&lt;/span&gt;
    &lt;span class="n"&gt;Customer&lt;/span&gt;   &lt;span class="n"&gt;Customer&lt;/span&gt;
    &lt;span class="n"&gt;Total&lt;/span&gt;      &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:numeric(12,2);not null"`&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="s"&gt;`gorm:"size:20;not null;default:'new'"`&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;&lt;span class="nv"&gt;$ &lt;/span&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt;            &lt;span class="c"&gt;# db.AutoMigrate(&amp;amp;Customer{}, &amp;amp;Order{}) on shop_dev&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pg_dump &lt;span class="nt"&gt;--schema-only&lt;/span&gt; &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="nt"&gt;--no-privileges&lt;/span&gt; shop_dev &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; db/schema.sql
&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
Starting a throwaway postgresql database &lt;span class="o"&gt;(&lt;/span&gt;postgres:17-alpine&lt;span class="o"&gt;)&lt;/span&gt;...
Read db/schema.sql: 2 table&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
No snapshot yet: using the current database schema as the baseline.
Created db/migrations/0001_initial.sql:
  - create table customers
  - create table orders
  - add index idx_orders_customer_id on orders
  - add foreign key fk_orders_customer on orders
Rollback script written to db/migrations/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why not let &lt;code&gt;AutoMigrate&lt;/code&gt; change the real database directly? It is fine in development, and GORM is upfront about its limits: its &lt;a href="https://gorm.io/docs/migration.html" rel="noopener noreferrer"&gt;migration docs&lt;/a&gt; (as of October 2026) say it "WON'T delete unused columns to protect your data" and that "at some point you may need to switch to a versioned migrations strategy". With the dump in between, &lt;code&gt;AutoMigrate&lt;/code&gt; stays where it is convenient, and production gets reviewed, numbered migrations with rollback scripts, renames that keep their data, and drops that need your confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  In CI
&lt;/h2&gt;

&lt;p&gt;Two lines catch a model change that has no migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python export_schema.py &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; db/schema.sql   &lt;span class="c"&gt;# or: npx drizzle-kit export ..., pg_dump ...&lt;/span&gt;
migrax check                              &lt;span class="c"&gt;# exit code 2: a change has no migration yet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit &lt;code&gt;db/schema.sql&lt;/code&gt; and the &lt;code&gt;.migrax/&lt;/code&gt; folder with every migration. While the schema file is unchanged, &lt;code&gt;migrax check&lt;/code&gt; reads the saved model and needs no Docker at all. Migrax itself runs on Java 17 or newer; a Python, Node or Go build that has no Java can use the container image &lt;code&gt;ghcr.io/fsmutimeer/migrax&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the migration before you commit it.&lt;/strong&gt; It is plain SQL, generated, but yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Views, functions, triggers and sequences&lt;/strong&gt; don't belong in the schema file. Keep them in repeatable migrations (&lt;code&gt;R__*.sql&lt;/code&gt;); identity columns work in the file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reshaping data is yours to write.&lt;/strong&gt; Migrax moves the structure and keeps values through renames. Splitting &lt;code&gt;full_name&lt;/code&gt; into first and last name needs a rule only you know: a migration written with &lt;code&gt;migrax new&lt;/code&gt;, between two generated ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL and MariaDB work the same way.&lt;/strong&gt; Compile with SQLAlchemy's &lt;code&gt;mysql&lt;/code&gt; dialect, or dump with &lt;code&gt;mysqldump --no-data --skip-comments&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your ORM already describes the schema. Export it as SQL into a schema file: a short script for SQLAlchemy, &lt;code&gt;drizzle-kit export&lt;/code&gt; for Drizzle, &lt;code&gt;pg_dump&lt;/code&gt; after &lt;code&gt;AutoMigrate&lt;/code&gt; for GORM.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax generate&lt;/code&gt; writes the next migration and its rollback script, asks about renames, and refuses to drop data without your confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax check&lt;/code&gt; in CI catches a model change without a migration, and &lt;code&gt;migrax verify&lt;/code&gt; proves the migrations build exactly the schema in the file.&lt;/li&gt;
&lt;li&gt;The migrations are plain SQL. Your application runs without Migrax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full reference, with MySQL and more ORMs: &lt;a href="https://migrax.org/guides/schema-from-orm/" rel="noopener noreferrer"&gt;Schema file from your ORM&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/generated-migrations-for-python-typescript-and-go/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>go</category>
      <category>database</category>
    </item>
    <item>
      <title>Migrax vs Flyway: generated vs hand-written migrations</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:54:44 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/migrax-vs-flyway-generated-vs-hand-written-migrations-126j</link>
      <guid>https://dev.to/fsmutimeer/migrax-vs-flyway-generated-vs-hand-written-migrations-126j</guid>
      <description>&lt;p&gt;Flyway and Migrax both keep a database schema in numbered SQL files, apply them in order and record what ran. The difference is where the SQL comes from. With Flyway you write every migration yourself. With Migrax you change your JPA entities and it writes the migration, which you then review.&lt;/p&gt;

&lt;p&gt;This article makes the same change with both, on the same PostgreSQL database, and compares what each one does along the way. It also says where Flyway is the better choice.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Flyway Community 13.12.0 (the open-source command line, released 2026-10-30), Migrax 0.17.1, Spring Boot 3.5.7, Hibernate 6.6.33, PostgreSQL 17. Compared on 2026-10-31. Statements about Flyway's editions come from Redgate's &lt;a href="https://documentation.red-gate.com/fd/feature-summary-165740620.html" rel="noopener noreferrer"&gt;feature summary&lt;/a&gt; (last updated 2026-10-27). Every output below was copied from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The change
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;customers&lt;/code&gt; table with two rows, and the change from &lt;a href="https://migrax.org/blog/stop-using-ddl-auto-update-in-production/" rel="noopener noreferrer"&gt;Stop using &lt;code&gt;ddl-auto=update&lt;/code&gt; in production&lt;/a&gt;: &lt;code&gt;name&lt;/code&gt; becomes &lt;code&gt;full_name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt; gets longer, and every customer needs a &lt;code&gt;country&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  With Flyway
&lt;/h2&gt;

&lt;p&gt;You write the migration. A correct version keeps the data by renaming the column, and adds the required column in three steps because the table has rows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;V2__rename_name_add_country.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;full_name&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;customers&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;country&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;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'GB'&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;customers&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;country&lt;/span&gt; &lt;span class="k"&gt;SET&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;320&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;&lt;span class="nv"&gt;$ &lt;/span&gt;flyway migrate
Current version of schema &lt;span class="s2"&gt;"public"&lt;/span&gt;: 1
Migrating schema &lt;span class="s2"&gt;"public"&lt;/span&gt; to version &lt;span class="s2"&gt;"2 - rename name add country"&lt;/span&gt;
Successfully applied 1 migration to schema &lt;span class="s2"&gt;"public"&lt;/span&gt;, now at version v2 &lt;span class="o"&gt;(&lt;/span&gt;execution &lt;span class="nb"&gt;time &lt;/span&gt;00:00.019s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flyway runs what you wrote, exactly as written. Whether the file matches the entities, and whether it keeps the data, is up to you and your reviewers. If you forget the &lt;code&gt;UPDATE&lt;/code&gt;, or write &lt;code&gt;DROP COLUMN name&lt;/code&gt; and &lt;code&gt;ADD COLUMN full_name&lt;/code&gt; instead of a rename, Flyway runs that too.&lt;/p&gt;

&lt;p&gt;There is no way back with the open-source edition:&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;flyway undo
ERROR: Flyway Redgate Edition Required: undo is not supported by OSS Edition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undo scripts (&lt;code&gt;U2__...sql&lt;/code&gt;) and the &lt;code&gt;undo&lt;/code&gt; command are part of Flyway Teams and Enterprise, and you write each undo script yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  With Migrax
&lt;/h2&gt;

&lt;p&gt;You change the entity (&lt;code&gt;fullName&lt;/code&gt;, &lt;code&gt;length = 320&lt;/code&gt;, a new &lt;code&gt;country&lt;/code&gt; field) and run &lt;code&gt;generate&lt;/code&gt;:&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;migrax generate &lt;span class="nt"&gt;--rename&lt;/span&gt; customers.name&lt;span class="o"&gt;=&lt;/span&gt;full_name
warning: Adding NOT NULL column &lt;span class="s1"&gt;'customers.country'&lt;/span&gt; without a default fails &lt;span class="k"&gt;if &lt;/span&gt;the table has rows. Add a default &lt;span class="k"&gt;in &lt;/span&gt;the SQL, or add it as nullable, backfill, &lt;span class="k"&gt;then &lt;/span&gt;make it NOT NULL.
Created src/main/resources/db/migration/0002_rename_customers_name_and_more.sql:
  - rename column customers.name to full_name
  - add column customers.country
  - alter column customers.email
Rollback script written to src/main/resources/db/migration/rollback.
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes the forward migration and a rollback script, and lints the migration before anything runs. The warning above is the &lt;code&gt;UPDATE&lt;/code&gt; you would otherwise have to remember. You edit the file the same way as the Flyway one, and end up with the same five statements. Without &lt;code&gt;--rename&lt;/code&gt;, Migrax sees a column go away and another appear, asks whether it was a rename, and refuses to drop the column without &lt;code&gt;--allow-destructive&lt;/code&gt;. In CI, where no one can answer, it stops and prints the &lt;code&gt;--rename&lt;/code&gt; option to use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;migrax rollback&lt;/code&gt; runs the generated rollback script:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rollback/0002_rename_customers_name_and_more.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;country&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;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the work moves from writing SQL to reviewing it. You still read every migration; you no longer type it, or its undo script.&lt;/p&gt;

&lt;h2&gt;
  
  
  What both do
&lt;/h2&gt;

&lt;p&gt;Much of the day-to-day is the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain SQL files in order.&lt;/strong&gt; Flyway's &lt;code&gt;V1__...sql&lt;/code&gt;, Migrax's &lt;code&gt;0001_...sql&lt;/code&gt;. Both also run Java migrations for changes SQL can't express.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A history table with checksums.&lt;/strong&gt; Both refuse to continue when someone edits a migration that already ran:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```shell $ flyway validate Migration checksum mismatch for migration version 1 -&amp;gt; Applied to database : -328024399 -&amp;gt; Resolved locally    : 1803351363 Either revert the changes to the migration, or run repair to update the schema history.&lt;/p&gt;

&lt;p&gt;$ migrax migrate error[MXE104]: Checksum changed for applied migration V1__create_customers.sql ```&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Baselines&lt;/strong&gt; for a database that existed before the tool: &lt;code&gt;flyway baseline&lt;/code&gt; and &lt;code&gt;migrax baseline&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running at application startup&lt;/strong&gt;, through Spring Boot's built-in Flyway support or the Migrax starter, and both have Maven and Gradle plugins.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where they differ
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Flyway Community&lt;/th&gt;
&lt;th&gt;Flyway Teams / Enterprise&lt;/th&gt;
&lt;th&gt;Migrax&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who writes the SQL&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;You; Enterprise can generate it from a schema model&lt;/td&gt;
&lt;td&gt;Generated from JPA entities, you review it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Undo / rollback&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Teams and Enterprise: undo scripts you write&lt;/td&gt;
&lt;td&gt;Rollback script generated with each migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dry run script&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;migrax migrate --sql&lt;/code&gt; prints the script without running it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift detection&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;migrax drift&lt;/code&gt;, and a check at startup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint for risky statements&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Not in the feature summary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;migrax lint&lt;/code&gt;, run on every &lt;code&gt;generate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throwaway-database check&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Not in the feature summary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;migrax verify&lt;/code&gt; applies every migration and rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Databases&lt;/td&gt;
&lt;td&gt;About 30, including Snowflake, BigQuery and MongoDB (&lt;a href="https://documentation.red-gate.com/fd/supported-databases-and-versions-143754067.html" rel="noopener noreferrer"&gt;list&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Same, with Redgate support&lt;/td&gt;
&lt;td&gt;12 relational databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Apache 2.0, every feature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Not in the feature summary" means Redgate's feature summary doesn't list it; Redgate's documentation describes the checks each edition has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Flyway is the better choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;There are no JPA entities.&lt;/strong&gt; Migrax generates migrations from Hibernate, JPA and a few other Java mappings (or a SQL schema file). A project that writes SQL by hand, or isn't on the JVM, gains less from it, while Flyway works the same for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The database isn't one of Migrax's 12.&lt;/strong&gt; Flyway reaches warehouses and document databases such as Snowflake, BigQuery, Databricks and MongoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a vendor behind the tool.&lt;/strong&gt; Redgate sells support, a desktop application and compliance features. Migrax is an open-source project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The team prefers writing every statement.&lt;/strong&gt; Some teams want the migration to be the source of truth, not the entities. Flyway is built around that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Switching from Flyway
&lt;/h2&gt;

&lt;p&gt;Migrax reads Flyway's history table, so a project can switch without rerunning anything. Copy or keep the &lt;code&gt;V*__&lt;/code&gt; files in the migration folder and run:&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;migrax import flyway
Recorded 2 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; as applied.
Next: disable Flyway &lt;span class="o"&gt;(&lt;/span&gt;spring.flyway.enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax generate'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; its first run uses the current database as the baseline.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax status
Migrations &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration &lt;span class="k"&gt;for &lt;/span&gt;jdbc:postgresql://localhost:5432/shop_fw
  &lt;span class="o"&gt;[&lt;/span&gt;X] V1__create_customers.sql         applied 2026-10-31 11:45:20
  &lt;span class="o"&gt;[&lt;/span&gt;X] V2__rename_name_add_country.sql  applied 2026-10-31 11:45:20
2 applied.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
No snapshot yet: using the current database schema as the baseline.
No changes detected.
Saved the entity schema as the baseline snapshot: .migrax/snapshot.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old files keep working, and the next entity change becomes &lt;code&gt;0003_...sql&lt;/code&gt;. Each other environment needs &lt;code&gt;migrax import flyway&lt;/code&gt; once. The guide &lt;a href="https://migrax.org/guides/switching-tools/" rel="noopener noreferrer"&gt;Switching from Flyway or Liquibase&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Flyway applies the SQL you write. Migrax writes the SQL from your entities, with a rollback script and lint warnings, and you review it.&lt;/li&gt;
&lt;li&gt;Both keep a checksummed history, refuse edited migrations, baseline existing databases and run at startup.&lt;/li&gt;
&lt;li&gt;Undo, dry-run scripts and drift detection are paid Flyway features; in Migrax they are part of the free tool.&lt;/li&gt;
&lt;li&gt;Flyway is the better fit without JPA entities, for databases Migrax doesn't support, or when you want commercial support.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/migrax-vs-flyway/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>flyway</category>
      <category>database</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Catching schema drift before it breaks production</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:54:01 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/catching-schema-drift-before-it-breaks-production-1oid</link>
      <guid>https://dev.to/fsmutimeer/catching-schema-drift-before-it-breaks-production-1oid</guid>
      <description>&lt;p&gt;Migrations only describe a database as long as nobody changes it any other way. Then someone adds a column during an incident, a DBA widens a type, a data import relaxes a constraint, and nobody writes it down. The next migration was tested against a database that production no longer is.&lt;/p&gt;

&lt;p&gt;That gap is called drift. This article makes four changes by hand on a production database, finds them with &lt;code&gt;migrax drift&lt;/code&gt;, keeps the one that was wanted, undoes the rest, and then makes the application refuse to start on a database that has drifted.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.1, Spring Boot 3.5.7, Hibernate 6.6.33, PostgreSQL 17. Every output below was copied from a real run; log lines are shortened to the message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;A Spring Boot application with a &lt;code&gt;customers&lt;/code&gt; table, built by two migrations and applied to production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Id&lt;/span&gt; &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fullName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GB"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here production is the database &lt;code&gt;shop_prod&lt;/code&gt; and development is &lt;code&gt;shop&lt;/code&gt;, on the same PostgreSQL server. &lt;code&gt;.migrax/snapshot.json&lt;/code&gt; records the schema those migrations produce. &lt;code&gt;generate&lt;/code&gt; keeps it up to date and it is committed with the migrations, so it is also the yardstick for drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four changes nobody wrote down
&lt;/h2&gt;

&lt;p&gt;During a busy week, four statements run on production by hand:&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;customers&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;vip&lt;/span&gt; &lt;span class="nb"&gt;boolean&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;false&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;customers&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;full_name&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;200&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;customers_country_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is a hotfix for a feature that is coming anyway. The second was needed for a data import and never reverted. The third was a mistake. The fourth sped up a slow report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding them
&lt;/h2&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;migrax drift &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
3 difference&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; between the database and the snapshot:
  - customers.email: expected varchar&lt;span class="o"&gt;(&lt;/span&gt;320&lt;span class="o"&gt;)&lt;/span&gt; but the database column is shorter &lt;span class="o"&gt;(&lt;/span&gt;varchar&lt;span class="o"&gt;(&lt;/span&gt;200&lt;span class="o"&gt;))&lt;/span&gt;
  - customers.full_name: expected NOT NULL but the database has NULL
  - customers.vip: column exists &lt;span class="k"&gt;in &lt;/span&gt;the database but not &lt;span class="k"&gt;in &lt;/span&gt;the expected schema
Someone changed the database outside migrations. Write a migration &lt;span class="k"&gt;for &lt;/span&gt;intended changes &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrax new &amp;lt;name&amp;gt;'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;, or revert them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;drift&lt;/code&gt; reads the live schema and compares it with the snapshot. It checks tables, columns, types, nullability, primary and foreign keys and unique constraints. Plain indexes are not part of that comparison, so &lt;code&gt;customers_country_idx&lt;/code&gt; isn't listed; &lt;code&gt;migrax diff&lt;/code&gt;, further down, does show it.&lt;/p&gt;

&lt;p&gt;The shorter &lt;code&gt;email&lt;/code&gt; is the dangerous one. The entity allows 320 characters, so the first customer with a long address gets an error from the database. Nothing in the code or the migrations would show why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;drift&lt;/code&gt; exits with code 2 when it finds differences, and &lt;code&gt;--json&lt;/code&gt; gives the same result for a script:&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;migrax drift &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"against"&lt;/span&gt;:&lt;span class="s2"&gt;"snapshot"&lt;/span&gt;,&lt;span class="s2"&gt;"pending"&lt;/span&gt;:0,&lt;span class="s2"&gt;"differences"&lt;/span&gt;:[&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;:&lt;span class="s2"&gt;"type"&lt;/span&gt;,&lt;span class="s2"&gt;"object"&lt;/span&gt;:&lt;span class="s2"&gt;"customers.email"&lt;/span&gt;,&lt;span class="s2"&gt;"detail"&lt;/span&gt;:&lt;span class="s2"&gt;"expected varchar(320) but the database column is shorter (varchar(200))"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;:&lt;span class="s2"&gt;"nullability"&lt;/span&gt;,&lt;span class="s2"&gt;"object"&lt;/span&gt;:&lt;span class="s2"&gt;"customers.full_name"&lt;/span&gt;,&lt;span class="s2"&gt;"detail"&lt;/span&gt;:&lt;span class="s2"&gt;"expected NOT NULL but the database has NULL"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;:&lt;span class="s2"&gt;"extra_column"&lt;/span&gt;,&lt;span class="s2"&gt;"object"&lt;/span&gt;:&lt;span class="s2"&gt;"customers.vip"&lt;/span&gt;,&lt;span class="s2"&gt;"detail"&lt;/span&gt;:&lt;span class="s2"&gt;"column exists in the database but not in the expected schema"&lt;/span&gt;&lt;span class="o"&gt;}]}&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--entities&lt;/code&gt;, &lt;code&gt;drift&lt;/code&gt; compares the database with the entities instead of the snapshot. That answers a different question: not "did someone change the database?" but "does the database fit the code I have now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the change you want
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;vip&lt;/code&gt; column is wanted, so it belongs in the entity and in a migration, like every other change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@ColumnDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;vip&lt;/span&gt;&lt;span class="o"&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;&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
Created src/main/resources/db/migration/0003_add_customers_vip.sql:
  - add column customers.vip
Rollback script written to src/main/resources/db/migration/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&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 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;customers&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;vip&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&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;Every other database (development, staging, a new laptop) needs this statement. Production already has the column and would fail on it, so add &lt;code&gt;IF NOT EXISTS&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;customers&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;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vip&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&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;The migration now runs everywhere, and production records it like any other:&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;migrax migrate &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
Applying migration &lt;span class="s1"&gt;'0003_add_customers_vip.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0003_add_customers_vip.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 3 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax drift &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
2 difference&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; between the database and the snapshot:
  - customers.email: expected varchar&lt;span class="o"&gt;(&lt;/span&gt;320&lt;span class="o"&gt;)&lt;/span&gt; but the database column is shorter &lt;span class="o"&gt;(&lt;/span&gt;varchar&lt;span class="o"&gt;(&lt;/span&gt;200&lt;span class="o"&gt;))&lt;/span&gt;
  - customers.full_name: expected NOT NULL but the database has NULL
Someone changed the database outside migrations. Write a migration &lt;span class="k"&gt;for &lt;/span&gt;intended changes &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrax new &amp;lt;name&amp;gt;'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;, or revert them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Undoing the rest
&lt;/h2&gt;

&lt;p&gt;For the changes that shouldn't be there, &lt;code&gt;migrax diff&lt;/code&gt; writes the SQL. It compares two live databases: the source is one that is right (here the development database, which only ever got migrations), the target is production. The source's credentials come from &lt;code&gt;MIGRAX_SOURCE_USER&lt;/code&gt; and &lt;code&gt;MIGRAX_SOURCE_PASSWORD&lt;/code&gt;, or &lt;code&gt;--source-user&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;--source-password-file&lt;/code&gt;:&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;migrax diff jdbc:postgresql://localhost:5432/shop &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
To make jdbc:postgresql://localhost:5432/shop_prod match jdbc:postgresql://localhost:5432/shop:
3 change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;postgresql:
  1. drop index customers_country_idx on customers  &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE]
     DROP INDEX customers_country_idx&lt;span class="p"&gt;;&lt;/span&gt;
  2. alter column customers.email
     ALTER TABLE customers ALTER COLUMN email TYPE varchar&lt;span class="o"&gt;(&lt;/span&gt;320&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  3. alter column customers.full_name
     ALTER TABLE customers ALTER COLUMN full_name SET NOT NULL&lt;span class="p"&gt;;&lt;/span&gt;
To keep them, save the SQL as a migration: migrax diff &amp;lt;source-url&amp;gt; &lt;span class="nt"&gt;--sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; also compares indexes, so the report index shows up here. &lt;code&gt;--sql&lt;/code&gt; prints the same statements as one script to review and hand to whoever runs SQL on production:&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;-- Generated by Migrax 0.17.1 for postgresql&lt;/span&gt;

&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;customers_country_idx&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;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;320&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;customers&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;full_name&lt;/span&gt; &lt;span class="k"&gt;SET&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;Keep the lines you mean to run. If the index is worth having, leave out the &lt;code&gt;DROP INDEX&lt;/code&gt; and add the index to the entity (&lt;code&gt;@Table(indexes = ...)&lt;/code&gt;) so the next &lt;code&gt;generate&lt;/code&gt; writes it for everyone else. &lt;code&gt;SET NOT NULL&lt;/code&gt; fails if the import left rows without a name, so fill those in first. Once the script has run:&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;migrax drift &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
No drift: the database matches the snapshot.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax diff jdbc:postgresql://localhost:5432/shop &lt;span class="nt"&gt;--url&lt;/span&gt; jdbc:postgresql://localhost:5432/shop_prod
Same schema: jdbc:postgresql://localhost:5432/shop_prod matches jdbc:postgresql://localhost:5432/shop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Noticing it the next time
&lt;/h2&gt;

&lt;p&gt;Finding drift by chance is too late. There are two places to check for it on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a schedule.&lt;/strong&gt; Because &lt;code&gt;drift&lt;/code&gt; exits with 2, a nightly CI job or Kubernetes CronJob fails when it finds something, and the usual alert for a failed job tells you the next morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When the application starts.&lt;/strong&gt; With the &lt;a href="https://migrax.org/frameworks/spring-boot/" rel="noopener noreferrer"&gt;Spring Boot starter&lt;/a&gt;, the application runs the same comparison each time it starts, right after it applies any pending migrations. Turn it on with one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;migrax.drift.check&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application reads the expected schema from &lt;code&gt;META-INF/migrax/snapshot.json&lt;/code&gt;, so the build has to put the snapshot there. With Maven, that is the &lt;code&gt;package-snapshot&lt;/code&gt; goal of the Migrax plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.migrax&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;migrax&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.17.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;package-snapshot&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The Gradle plugin and the Quarkus extension copy it without any setup.)&lt;/p&gt;

&lt;p&gt;A week later someone adds another column by hand, and the next deployment stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrax: schema is up to date
Migrax: 1 difference(s) between the database and the schema its migrations produce:
  - customers.hotfix_note: column exists in the database but not in the expected schema
Someone changed the database outside migrations. Write a migration for intended changes, or revert them.
(migrax.drift.check=fail stops the start; warn only logs.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change is noticed at the deployment, not weeks later when something breaks. On a database without drift, the log says so and the application starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrax: schema is up to date
Migrax: no drift: the database matches the schema its migrations produce
Started ShopApplication in 6.757 seconds (process running for 7.945)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrax.drift.check=warn&lt;/code&gt; logs the same differences without stopping the start, which is a gentler way to begin. Quarkus, Micronaut, Helidon and Jakarta EE take the same setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;migrax drift&lt;/code&gt; compares a live database with the schema its migrations produce and exits with 2 when someone changed it by hand. &lt;code&gt;--json&lt;/code&gt; is for scripts, &lt;code&gt;--entities&lt;/code&gt; compares with the code instead.&lt;/li&gt;
&lt;li&gt;A hand-made change you want goes into the entity and a migration. &lt;code&gt;IF NOT EXISTS&lt;/code&gt; lets the database that already has it run the migration too.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax diff&lt;/code&gt; writes the SQL that undoes the rest, by comparing with a database that only ever got migrations. Unlike &lt;code&gt;drift&lt;/code&gt;, it also compares indexes.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;drift&lt;/code&gt; on a schedule, or set &lt;code&gt;migrax.drift.check=fail&lt;/code&gt; so the application doesn't start on a drifted database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More in the guides: &lt;a href="https://migrax.org/guides/production-safety/" rel="noopener noreferrer"&gt;Production safety&lt;/a&gt; and &lt;a href="https://migrax.org/guides/comparing-databases/" rel="noopener noreferrer"&gt;Comparing databases&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/catching-schema-drift/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Stop using ddl-auto=update in production</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:53:11 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/stop-using-ddl-autoupdate-in-production-1n65</link>
      <guid>https://dev.to/fsmutimeer/stop-using-ddl-autoupdate-in-production-1n65</guid>
      <description>&lt;p&gt;&lt;code&gt;spring.jpa.hibernate.ddl-auto=update&lt;/code&gt; is the fastest way to get started: change an entity, restart, and the table follows. It stays in the configuration long after the first demo, and one day it runs against a database with real customers in it.&lt;/p&gt;

&lt;p&gt;This article makes three ordinary entity changes with &lt;code&gt;update&lt;/code&gt; switched on and shows what Hibernate does to the database. Then it moves the same project, with its existing data, to generated migrations that you review before they run.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.1, Spring Boot 3.5.7, Hibernate 6.6.33, PostgreSQL 17. Every output below was copied from a real run; log lines are shortened to the message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;A Spring Boot application with one entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Id&lt;/span&gt; &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the setting this article is about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.jpa.hibernate.ddl-auto&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first start Hibernate creates the table. Two customers sign up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |      email       |     name
----+------------------+--------------
  1 | ada@example.com  | Ada Lovelace
  2 | alan@example.com | Alan Turing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far &lt;code&gt;update&lt;/code&gt; has done exactly what it promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ordinary changes
&lt;/h2&gt;

&lt;p&gt;The next release changes the entity in three small ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; becomes &lt;code&gt;fullName&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; gets longer (&lt;code&gt;length = 320&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;every customer now needs a &lt;code&gt;country&lt;/code&gt; (&lt;code&gt;nullable = false&lt;/code&gt;, &lt;code&gt;"GB"&lt;/code&gt; by default in Java).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fullName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"GB"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application starts, and the next customer tries to sign up. With &lt;code&gt;logging.level.org.hibernate.SQL=debug&lt;/code&gt;, the log shows what &lt;code&gt;update&lt;/code&gt; did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alter table if exists customers add column country varchar(2) not null
GenerationTarget encountered exception accepting command : Error executing DDL "alter table
  if exists customers add column country varchar(2) not null" via JDBC [ERROR: column
  "country" of relation "customers" contains null values]
alter table if exists customers alter column email set data type varchar(320)
alter table if exists customers add column full_name varchar(100) not null
GenerationTarget encountered exception accepting command : Error executing DDL "alter table
  if exists customers add column full_name varchar(100) not null" via JDBC [ERROR: column
  "full_name" of relation "customers" contains null values]
Started ShopApplication in 5.803 seconds (process running for 6.91)
insert into customers (country,email,full_name) values (?,?,?)
ERROR: column "country" of relation "customers" does not exist
Application run failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happened, and only one of them was what you meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The rename isn't a rename.&lt;/strong&gt; Hibernate has no way to know that &lt;code&gt;full_name&lt;/code&gt; is the old &lt;code&gt;name&lt;/code&gt;. It tries to add a new, empty column next to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding the required columns fails.&lt;/strong&gt; PostgreSQL can't add a &lt;code&gt;NOT NULL&lt;/code&gt; column without a default to a table that already has rows. Hibernate logs a warning and carries on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The application starts anyway.&lt;/strong&gt; The failures are logged as warnings and startup carries on. The error only shows up when the first customer signs up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only the longer &lt;code&gt;email&lt;/code&gt; worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quick fix makes it worse
&lt;/h2&gt;

&lt;p&gt;The obvious way out is to drop &lt;code&gt;nullable = false&lt;/code&gt; from the two new fields so Hibernate can add them. That works, in the sense that the columns appear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alter table if exists customers add column country varchar(2)
alter table if exists customers add column full_name varchar(100)
Started ShopApplication in 6.557 seconds (process running for 7.569)
insert into customers (country,email,full_name) values (?,?,?)
ERROR: null value in column "name" of relation "customers" violates not-null constraint
Application run failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old &lt;code&gt;name&lt;/code&gt; column is still there, still &lt;code&gt;NOT NULL&lt;/code&gt;, and the entity no longer writes to it. Every signup fails. The table now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |      email       |     name     | country | full_name
----+------------------+--------------+---------+-----------
  1 | ada@example.com  | Ada Lovelace |         |
  2 | alan@example.com | Alan Turing  |         |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing customers have no &lt;code&gt;full_name&lt;/code&gt; and no &lt;code&gt;country&lt;/code&gt;, so the application shows them without a name. Nothing recorded what changed or when, and there is no way to undo it except by hand.&lt;/p&gt;

&lt;p&gt;None of this is a bug in Hibernate. &lt;code&gt;update&lt;/code&gt; only ever adds: it never renames, never drops, never moves data, and doesn't stop the application when a statement fails. That is a safe choice for a tool that runs on every start without asking anyone, and it is the reason it shouldn't touch a database whose data matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching to migrations
&lt;/h2&gt;

&lt;p&gt;The same change, done with Migrax. The database goes back to where it was before the release: two customers and a &lt;code&gt;name&lt;/code&gt; column. The entity is back at version 1 too, because the switch is easiest between releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Let &lt;code&gt;doctor&lt;/code&gt; find the setting
&lt;/h3&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;migrax doctor
...
  &lt;span class="o"&gt;[&lt;/span&gt;warn]  Hibernate will change your database schema by itself
          Found:  spring.jpa.hibernate.ddl-auto &lt;span class="o"&gt;=&lt;/span&gt; update
                  &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/application.properties, line 4

          Why:    Hibernate adds tables and columns itself when the application starts. Migrations
                  &lt;span class="k"&gt;then &lt;/span&gt;fail with &lt;span class="s2"&gt;"already exists"&lt;/span&gt;, and the database drifts from what your migrations
                  describe, so other environments end up different.

          Fix:    change   spring.jpa.hibernate.ddl-auto&lt;span class="o"&gt;=&lt;/span&gt;update
                  to       spring.jpa.hibernate.ddl-auto&lt;span class="o"&gt;=&lt;/span&gt;none
                  &lt;span class="o"&gt;(&lt;/span&gt;or validate: Hibernate &lt;span class="k"&gt;then &lt;/span&gt;checks the schema at startup without changing it&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt;info]  Snapshot: none yet&lt;span class="p"&gt;;&lt;/span&gt; the first generate uses the database as baseline
  &lt;span class="o"&gt;[&lt;/span&gt;ok]    Database URL: jdbc:postgresql://localhost:5432/shop
  &lt;span class="o"&gt;[&lt;/span&gt;ok]    Connected: PostgreSQL 17.11 &lt;span class="o"&gt;(&lt;/span&gt;Debian 17.11-1.pgdg13+2&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;driver 42.7.8&lt;span class="o"&gt;)&lt;/span&gt;
...
All checks passed, with 1 warning&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; above.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrax reads the database settings from &lt;code&gt;spring.datasource.*&lt;/code&gt;, so there is nothing else to configure. Change the line to &lt;code&gt;validate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.jpa.hibernate.ddl-auto&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;validate&lt;/code&gt;, Hibernate still checks at startup that every entity matches a table and column, and refuses to start if one doesn't. It just never changes anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Record what the database already has
&lt;/h3&gt;

&lt;p&gt;The first &lt;code&gt;generate&lt;/code&gt; has nothing to compare with yet, so it uses the live database:&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;migrax generate
No snapshot yet: using the current database schema as the baseline.
No changes detected.
Created src/main/resources/db/migration/0001_baseline.sql with the 1 table&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; the database already has.
  It is recorded as applied &lt;span class="k"&gt;in &lt;/span&gt;this database without running it&lt;span class="p"&gt;;&lt;/span&gt; empty databases are created from it.
Saved the entity schema as the baseline snapshot: .migrax/snapshot.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0001_baseline.sql&lt;/code&gt; describes the table as it is:&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;-- Generated by Migrax 0.17.1 for postgresql&lt;/span&gt;
&lt;span class="c1"&gt;-- Baseline: the tables the database already had before its first migration.&lt;/span&gt;
&lt;span class="c1"&gt;-- That database records it as applied without running it; empty databases are created from it.&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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="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;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="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;customers_pkey&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="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uk_customers_email&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This database won't run it, because the table is already there. A new laptop, a test database or &lt;code&gt;migrax verify&lt;/code&gt; starts empty and builds the table from it. "No changes detected" means the entities and the database agree, so nothing is lost by switching. If &lt;code&gt;update&lt;/code&gt; had already left old columns behind, they would show up here as changes to review.&lt;/p&gt;

&lt;p&gt;Commit the migration folder and &lt;code&gt;.migrax/snapshot.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make the change again
&lt;/h3&gt;

&lt;p&gt;Now the same three changes to &lt;code&gt;Customer&lt;/code&gt;. This time &lt;code&gt;generate&lt;/code&gt; writes them down instead of running them:&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;migrax generate &lt;span class="nt"&gt;--rename&lt;/span&gt; customers.name&lt;span class="o"&gt;=&lt;/span&gt;full_name
warning: Adding NOT NULL column &lt;span class="s1"&gt;'customers.country'&lt;/span&gt; without a default fails &lt;span class="k"&gt;if &lt;/span&gt;the table has rows. Add a default &lt;span class="k"&gt;in &lt;/span&gt;the SQL, or add it as nullable, backfill, &lt;span class="k"&gt;then &lt;/span&gt;make it NOT NULL.
Created src/main/resources/db/migration/0002_rename_customers_name_and_more.sql:
  - rename column customers.name to full_name
  - add column customers.country
  - alter column customers.email
Rollback script written to src/main/resources/db/migration/rollback.
src/main/resources/db/migration/0002_rename_customers_name_and_more.sql, statement 1: warning MX008
  ALTER TABLE customers RENAME COLUMN name TO full_name
  Application instances still running the old code use the old name and will fail.
  ...
src/main/resources/db/migration/0002_rename_customers_name_and_more.sql, statement 2: warning MX002
  ALTER TABLE customers ADD COLUMN country varchar&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)&lt;/span&gt; NOT NULL
  Adding a NOT NULL column without a default fails when customers has rows.
  Fix: Add a DEFAULT, or add the column as nullable, backfill it, &lt;span class="k"&gt;then &lt;/span&gt;make it NOT NULL.
...
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;--rename&lt;/code&gt;, Migrax notices that a column went away while another appeared and asks whether it was a rename; in CI it refuses to guess and stops. &lt;a href="https://migrax.org/blog/renaming-columns-and-tables-without-losing-data/" rel="noopener noreferrer"&gt;Renaming columns and tables without losing data&lt;/a&gt; shows both.&lt;/p&gt;

&lt;p&gt;The warning MX002 is the failure from the &lt;code&gt;update&lt;/code&gt; run, found before anything ran. The migration is a plain SQL file, so fix it the way the warning says: add the column, fill it, then make it required.&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;-- Generated by Migrax 0.17.1 for postgresql&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;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;full_name&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;customers&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;country&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;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'GB'&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;customers&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;country&lt;/span&gt; &lt;span class="k"&gt;SET&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;320&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next to it, &lt;code&gt;rollback/0002_rename_customers_name_and_more.sql&lt;/code&gt; undoes the change:&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;customers&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;country&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;customers&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;email&lt;/span&gt; &lt;span class="k"&gt;TYPE&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Apply it
&lt;/h3&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 2 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application starts with &lt;code&gt;validate&lt;/code&gt;, the next customer signs up, and the existing ones kept their names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |       email       |  full_name   | country
----+-------------------+--------------+---------
  1 | ada@example.com   | Ada Lovelace | GB
  2 | alan@example.com  | Alan Turing  | GB
  3 | grace@example.com | Grace Hopper | GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The other environments
&lt;/h2&gt;

&lt;p&gt;Production, staging and every teammate's database have the same table but no migration history. Each one needs to be told once that the baseline is already there:&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;migrax status
Migrations &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration &lt;span class="k"&gt;for &lt;/span&gt;jdbc:postgresql://localhost:5432/shop_prod
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 0001_baseline.sql                        pending
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 0002_rename_customers_name_and_more.sql  pending
2 pending.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax baseline 0001 &lt;span class="nt"&gt;--yes&lt;/span&gt;
Recording 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; as applied without running them:
  0001_baseline.sql
Recorded. Run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt; to apply the migrations after 0001_baseline.sql.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax migrate
Applying migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 2 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Point each command at that database with &lt;code&gt;--url&lt;/code&gt;, or the environment variables your deployment already uses.)&lt;/p&gt;

&lt;p&gt;Forgetting &lt;code&gt;baseline&lt;/code&gt; is safe. &lt;code&gt;migrate&lt;/code&gt; tries to create the table, the database refuses, and nothing is recorded or changed:&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0001_baseline.sql'&lt;/span&gt;...
error: Failed executing migration &lt;span class="s1"&gt;'0001_baseline.sql'&lt;/span&gt;: ERROR: relation &lt;span class="s2"&gt;"customers"&lt;/span&gt; already exists
error[MXE102]: Migration &lt;span class="s1"&gt;'0001_baseline.sql'&lt;/span&gt; failed and was not recorded as applied. ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;baseline&lt;/code&gt; then refuses too, because the failed attempt counts as history. Record the baseline with &lt;code&gt;repair&lt;/code&gt; instead, and carry on:&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;migrax repair 0001_baseline.sql &lt;span class="nt"&gt;--action&lt;/span&gt; applied &lt;span class="nt"&gt;--yes&lt;/span&gt;
Repaired &lt;span class="nb"&gt;history &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;0001_baseline.sql &lt;span class="o"&gt;(&lt;/span&gt;action: applied&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Keep a record of this repair.

&lt;span class="nv"&gt;$ &lt;/span&gt;migrax migrate
Applying migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0002_rename_customers_name_and_more.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 2 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other frameworks
&lt;/h2&gt;

&lt;p&gt;The setting has a different name outside Spring Boot, and &lt;code&gt;migrax doctor&lt;/code&gt; finds each of them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Change to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spring.jpa.hibernate.ddl-auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validate&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarkus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;quarkus.hibernate-orm.schema-management.strategy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validate&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Micronaut&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jpa.default.properties.hibernate.hbm2ddl.auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validate&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain Hibernate, Jakarta EE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hibernate.hbm2ddl.auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;validate&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ddl-auto=update&lt;/code&gt; only adds. It turns a rename into a second, empty column, can't add a required column to a table with rows, and lets the application start when a statement fails.&lt;/li&gt;
&lt;li&gt;Switch between releases: set &lt;code&gt;validate&lt;/code&gt;, run &lt;code&gt;migrax generate&lt;/code&gt; once to record the existing tables as &lt;code&gt;0001_baseline.sql&lt;/code&gt;, and run &lt;code&gt;migrax baseline 0001 --yes&lt;/code&gt; once on every other database.&lt;/li&gt;
&lt;li&gt;From then on every change is a SQL file you review, with lint warnings for the statements that fail on real data, and a rollback script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More in the guides: &lt;a href="https://migrax.org/getting-started/quickstart/" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt; and &lt;a href="https://migrax.org/guides/production-safety/" rel="noopener noreferrer"&gt;Production safety&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/stop-using-ddl-auto-update-in-production/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Applying and undoing migrations: migrate, status, rollback</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:23:55 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/applying-and-undoing-migrations-migrate-status-rollback-86p</link>
      <guid>https://dev.to/fsmutimeer/applying-and-undoing-migrations-migrate-status-rollback-86p</guid>
      <description>&lt;p&gt;&lt;code&gt;migrax generate&lt;/code&gt; writes migrations, and three commands handle the rest: &lt;code&gt;status&lt;/code&gt; shows what the database has, &lt;code&gt;migrate&lt;/code&gt; applies what is missing, and &lt;code&gt;rollback&lt;/code&gt; undoes it. This article goes through each one on a real project. It also covers what a rollback can and can't bring back, and what happens when someone edits a migration that already ran.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.1, Hibernate 6.6.13, PostgreSQL 17. Every output below was copied from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where we start
&lt;/h2&gt;

&lt;p&gt;The shop project from the earlier articles has four applied migrations: the first tables, a &lt;code&gt;phone&lt;/code&gt; column, and the two renames from &lt;a href="https://migrax.org/blog/renaming-columns-and-tables-without-losing-data/" rel="noopener noreferrer"&gt;Renaming columns and tables without losing data&lt;/a&gt;. Now two more entity changes come in: a &lt;code&gt;notes&lt;/code&gt; field on &lt;code&gt;PurchaseOrder&lt;/code&gt;, and an index on its &lt;code&gt;createdAt&lt;/code&gt; column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"purchase_orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ix_orders_created"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"createdAt"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PurchaseOrder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;migrax generate&lt;/code&gt; once per change gives two files: &lt;code&gt;0005_add_purchase_orders_notes.sql&lt;/code&gt; and &lt;code&gt;0006_index_purchase_orders.sql&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;status&lt;/code&gt;: what has the database got?
&lt;/h2&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;migrax status
Migrations &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration &lt;span class="k"&gt;for &lt;/span&gt;jdbc:postgresql://localhost:5432/shop
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0001_initial.sql                    applied 2026-10-25 16:04:44
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0002_add_customers_phone.sql        applied 2026-10-25 16:04:58
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0003_rename_customers_name.sql      applied 2026-10-25 16:06:06
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0004_rename_orders.sql              applied 2026-10-25 16:07:06
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 0005_add_purchase_orders_notes.sql  pending
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; 0006_index_purchase_orders.sql      pending
4 applied, 2 pending.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[X]&lt;/code&gt; is applied, &lt;code&gt;[ ]&lt;/code&gt; is pending. Migrax keeps the history in a &lt;code&gt;migrax_history&lt;/code&gt; table in the same database, with a checksum of every applied file. &lt;code&gt;status&lt;/code&gt; only reads; it never changes anything. The alias &lt;code&gt;showmigrations&lt;/code&gt; does the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before applying: &lt;code&gt;--dry-run&lt;/code&gt; and &lt;code&gt;--sql&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--dry-run&lt;/code&gt; lists what would run and checks it for risky SQL, without touching the database:&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;migrax migrate &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
2 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; would be applied:
  0005_add_purchase_orders_notes.sql
  0006_index_purchase_orders.sql
src/main/resources/db/migration/0006_index_purchase_orders.sql, statement 1: warning MX001
  CREATE INDEX ix_orders_created ON purchase_orders &lt;span class="o"&gt;(&lt;/span&gt;createdAt&lt;span class="o"&gt;)&lt;/span&gt;
  Building this index blocks writes to purchase_orders &lt;span class="k"&gt;until &lt;/span&gt;it finishes.
  Fix: Use CREATE INDEX CONCURRENTLY &lt;span class="k"&gt;in &lt;/span&gt;its own migration that starts with &lt;span class="s1"&gt;'-- migrax:no-transaction'&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrax generate --safe'&lt;/span&gt; does this&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a small table that warning doesn't matter. On a table with millions of rows, it is the difference between a quick deploy and minutes of blocked writes. The &lt;a href="https://migrax.org/reference/lint-rules/" rel="noopener noreferrer"&gt;lint rules&lt;/a&gt; page lists every check.&lt;/p&gt;

&lt;p&gt;If a DBA has to run the changes, &lt;code&gt;--sql&lt;/code&gt; prints one script for them. It applies each migration in its own transaction and records it in the history, so Migrax sees it as applied afterwards:&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;migrax migrate &lt;span class="nt"&gt;--sql&lt;/span&gt;
&lt;span class="nt"&gt;--&lt;/span&gt; Migration script written by Migrax: 2 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; to apply.
&lt;span class="nt"&gt;--&lt;/span&gt; Review it, &lt;span class="k"&gt;then &lt;/span&gt;run the whole file with the database&lt;span class="s1"&gt;'s own tool; it must stop
-- at the first error:
--   psql -v ON_ERROR_STOP=1 -f &amp;lt;this file&amp;gt;
-- Each migration is recorded in migrax_history, so '&lt;/span&gt;migrax status&lt;span class="s1"&gt;' and later
-- '&lt;/span&gt;migrax migrate&lt;span class="s1"&gt;' runs see it as applied.

-- 0005_add_purchase_orders_notes.sql
BEGIN;
-- Generated by Migrax 0.17.1 for postgresql

ALTER TABLE purchase_orders ADD COLUMN notes varchar(500);
INSERT INTO migrax_history(version,checksum,applied_at) VALUES('&lt;/span&gt;0005_add_purchase_orders_notes.sql&lt;span class="s1"&gt;','&lt;/span&gt;5f26d7ff0f5e8502d49b57c00fd7449f0619bcf7d8f38441f006fcd261904837&lt;span class="s1"&gt;',(CURRENT_TIMESTAMP AT TIME ZONE '&lt;/span&gt;Etc/UTC&lt;span class="s1"&gt;'));
COMMIT;

-- 0006_index_purchase_orders.sql
...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;migrate&lt;/code&gt;: all at once, or up to a point
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;migrax migrate&lt;/code&gt; applies every pending migration in order. &lt;code&gt;--to&lt;/code&gt; stops after the one you name:&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;migrax migrate &lt;span class="nt"&gt;--to&lt;/span&gt; 0005_add_purchase_orders_notes.sql
Applying migration &lt;span class="s1"&gt;'0005_add_purchase_orders_notes.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0005_add_purchase_orders_notes.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; up to 0005_add_purchase_orders_notes.sql.
1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; after it stay pending.
&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="nv"&gt;$ &lt;/span&gt;migrax migrate
Applying migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 6 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On PostgreSQL each migration runs in a transaction. If a statement fails, that migration is rolled back as a whole and &lt;code&gt;migrate&lt;/code&gt; stops. Migrax also takes a lock on the database while it works, so two application instances that start at the same moment don't both apply the same migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;rollback&lt;/code&gt;: undoing the newest migrations
&lt;/h2&gt;

&lt;p&gt;Every generated migration has a rollback script next to it, in &lt;code&gt;rollback/&lt;/code&gt;. &lt;code&gt;migrax rollback&lt;/code&gt; runs the script of the newest applied migration and removes that migration from the history.&lt;/p&gt;

&lt;p&gt;Because a rollback can delete data, Migrax wants a confirmation. In a terminal it asks:&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;migrax rollback
Rolling back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0006_index_purchase_orders.sql
Roll back now? Data written to removed objects is lost. &lt;span class="o"&gt;[&lt;/span&gt;y/N] y
Rolling back migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolled back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a script or CI job it stops instead, until you add &lt;code&gt;--yes&lt;/code&gt;:&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;migrax rollback
Rolling back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0006_index_purchase_orders.sql
error[MXE001]: Rollback needs confirmation.
Back up the database, &lt;span class="k"&gt;then &lt;/span&gt;rerun with &lt;span class="nt"&gt;--yes&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--steps n&lt;/code&gt; undoes the newest &lt;code&gt;n&lt;/code&gt; migrations. &lt;code&gt;--dry-run&lt;/code&gt; shows the list first:&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;migrax rollback &lt;span class="nt"&gt;--steps&lt;/span&gt; 2 &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
Would roll back 2 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0006_index_purchase_orders.sql
  0005_add_purchase_orders_notes.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--to&lt;/code&gt; keeps the migration you name and undoes everything after it:&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;migrax rollback &lt;span class="nt"&gt;--to&lt;/span&gt; 0003_rename_customers_name.sql &lt;span class="nt"&gt;--yes&lt;/span&gt;
Rolling back 3 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0006_index_purchase_orders.sql
  0005_add_purchase_orders_notes.sql
  0004_rename_orders.sql
Rolling back migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0006_index_purchase_orders.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolling back migration &lt;span class="s1"&gt;'0005_add_purchase_orders_notes.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0005_add_purchase_orders_notes.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolling back migration &lt;span class="s1"&gt;'0004_rename_orders.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0004_rename_orders.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolled back 3 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolled-back migrations are pending again, so the next &lt;code&gt;migrax migrate&lt;/code&gt; applies them again. The files stay where they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a rollback can't bring back
&lt;/h2&gt;

&lt;p&gt;A rollback restores the structure, not the data. Say someone stores a note on an order:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;purchase_orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Gift wrap, please'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rolling back &lt;code&gt;0005&lt;/code&gt; drops the &lt;code&gt;notes&lt;/code&gt; column. Applying it again brings an empty column back, and the note is gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id | notes
----+-------
  1 |
  2 |
  3 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rollback script says so in its header, so it shows in review before anything runs:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/rollback/0005_add_purchase_orders_notes.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;-- Generated by Migrax 0.17.1 for postgresql&lt;/span&gt;
&lt;span class="c1"&gt;-- Rollback for 0005_add_purchase_orders_notes.sql. 'migrax rollback' runs it.&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: deletes data written to purchase_orders.notes since the migration ran.&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;purchase_orders&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies the other way round: when a migration drops a column, its rollback can re-create the column but not its old values. To keep those, generate that migration with &lt;code&gt;--keep-data&lt;/code&gt;. Migrax then copies the values into a backup table first, and the rollback puts them back (&lt;a href="https://migrax.org/guides/rollbacks-and-recovery/" rel="noopener noreferrer"&gt;Rollbacks and recovery&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't edit a migration that already ran
&lt;/h2&gt;

&lt;p&gt;Migrax stores a checksum of each applied file. If someone changes the file afterwards, even just by adding a comment, &lt;code&gt;status&lt;/code&gt; shows it and exits with code 1:&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;migrax status
Migrations &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration &lt;span class="k"&gt;for &lt;/span&gt;jdbc:postgresql://localhost:5432/shop
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0001_initial.sql                    applied 2026-10-25 16:04:44
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0002_add_customers_phone.sql        applied 2026-10-25 16:04:58
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0003_rename_customers_name.sql      applied 2026-10-25 16:06:06
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0004_rename_orders.sql              applied 2026-10-25 16:08:46
  &lt;span class="o"&gt;[!]&lt;/span&gt; 0005_add_purchase_orders_notes.sql  CHANGED: file changed after it was applied 2026-10-25 16:08:46
  &lt;span class="o"&gt;[&lt;/span&gt;X] 0006_index_purchase_orders.sql      applied 2026-10-25 16:08:46
5 applied, 1 changed.
Some migrations need attention. See &lt;span class="s1"&gt;'migrax help repair'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrate&lt;/code&gt; refuses to run at all:&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;migrax migrate
error[MXE104]: Checksum changed &lt;span class="k"&gt;for &lt;/span&gt;applied migration 0005_add_purchase_orders_notes.sql
Run again with &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This protects you from a database that no longer matches its files: the edited SQL never ran on the databases that already applied the old version. Put the file back as it was (here, &lt;code&gt;git checkout&lt;/code&gt; of the file) and write any new change as a new migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;migrax status&lt;/code&gt; shows applied, pending and changed migrations, and never changes anything.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax migrate&lt;/code&gt; applies pending migrations in order. &lt;code&gt;--dry-run&lt;/code&gt; previews and lints, &lt;code&gt;--sql&lt;/code&gt; writes a script for a DBA, and &lt;code&gt;--to&lt;/code&gt; stops at a given migration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax rollback&lt;/code&gt; undoes the newest migrations (&lt;code&gt;--steps&lt;/code&gt;, &lt;code&gt;--to&lt;/code&gt;). It asks first, and needs &lt;code&gt;--yes&lt;/code&gt; in scripts.&lt;/li&gt;
&lt;li&gt;Rollbacks restore structure, not data. The rollback script's header says what gets lost, and &lt;code&gt;--keep-data&lt;/code&gt; keeps it.&lt;/li&gt;
&lt;li&gt;Never edit an applied migration. Write a new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More in the guides: &lt;a href="https://migrax.org/guides/rollbacks-and-recovery/" rel="noopener noreferrer"&gt;Rollbacks and recovery&lt;/a&gt; and &lt;a href="https://migrax.org/guides/production-safety/" rel="noopener noreferrer"&gt;Production safety&lt;/a&gt;, which shows how &lt;code&gt;migrax verify&lt;/code&gt; tests every rollback script before release.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/applying-and-undoing-migrations/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>database</category>
      <category>postgres</category>
      <category>devops</category>
    </item>
    <item>
      <title>Renaming columns and tables without losing data</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:22:41 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/renaming-columns-and-tables-without-losing-data-5cj3</link>
      <guid>https://dev.to/fsmutimeer/renaming-columns-and-tables-without-losing-data-5cj3</guid>
      <description>&lt;p&gt;Renames are where generated migrations usually go wrong. When a field changes from &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt;, the database only sees one column disappear and another appear. A tool that just compares the two schemas drops the old column, and every value in it, then adds an empty new one.&lt;/p&gt;

&lt;p&gt;Migrax notices when a change looks like a rename. In a terminal it asks you. In CI, where no one can answer, it refuses to write a migration that would lose the data. This article shows both cases for a column and for a table, and how to undo a rename.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.17.1, Hibernate 6.6.13, PostgreSQL 17. Every output below was copied from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where we start
&lt;/h2&gt;

&lt;p&gt;This picks up where &lt;a href="https://migrax.org/blog/generate-sql-migrations-from-jpa-entities/" rel="noopener noreferrer"&gt;Generate SQL migrations from your JPA entities&lt;/a&gt; ended: a &lt;code&gt;customers&lt;/code&gt; table and an &lt;code&gt;orders&lt;/code&gt; table, two migrations applied, and a few rows of data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      email       | id |     name     |      phone
------------------+----+--------------+------------------
 ada@example.com  |  1 | Ada Lovelace | +44 20 7946 0001
 alan@example.com |  2 | Alan Turing  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each order points at its customer through a foreign key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renaming a column
&lt;/h2&gt;

&lt;p&gt;Rename the field in &lt;code&gt;Customer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"full_name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fullName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  In CI: Migrax refuses
&lt;/h3&gt;

&lt;p&gt;First, run &lt;code&gt;generate&lt;/code&gt; where nobody can answer a question, such as a CI job or a script:&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;migrax generate
Possible rename: customers.name to customers.full_name. To keep the data, rerun with &lt;span class="nt"&gt;--rename&lt;/span&gt; customers.name&lt;span class="o"&gt;=&lt;/span&gt;full_name.
2 change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;postgresql:
  1. drop column customers.name  &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE]
     ALTER TABLE customers DROP COLUMN name&lt;span class="p"&gt;;&lt;/span&gt;
  2. add column customers.full_name
     ALTER TABLE customers ADD COLUMN full_name varchar&lt;span class="o"&gt;(&lt;/span&gt;100&lt;span class="o"&gt;)&lt;/span&gt; NOT NULL&lt;span class="p"&gt;;&lt;/span&gt;
error[MXE108]: 1 destructive change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; detected &lt;span class="o"&gt;(&lt;/span&gt;marked &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE] above&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Review them, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax generate --allow-destructive'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; If a column or table was renamed, pass &lt;span class="nt"&gt;--rename&lt;/span&gt; table.old&lt;span class="o"&gt;=&lt;/span&gt;new or &lt;span class="nt"&gt;--rename-table&lt;/span&gt; &lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;new instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with code 1 and writes no file. Here, writing the plain drop and add would have been doubly wrong. It would have deleted both names. Then &lt;code&gt;ADD COLUMN ... NOT NULL&lt;/code&gt; would have failed anyway, because the existing rows have no value for the new column.&lt;/p&gt;

&lt;p&gt;The first line gives the option that keeps the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  In a terminal: Migrax asks
&lt;/h3&gt;

&lt;p&gt;Run the same command in a terminal and Migrax asks instead:&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;migrax generate
Did you rename customers.name to customers.full_name? &lt;span class="o"&gt;[&lt;/span&gt;y/N] y
Created src/main/resources/db/migration/0003_rename_customers_name.sql:
  - rename column customers.name to full_name
Rollback script written to src/main/resources/db/migration/rollback.
src/main/resources/db/migration/0003_rename_customers_name.sql, statement 1: warning MX008
  ALTER TABLE customers RENAME COLUMN name TO full_name
  Application instances still running the old code use the old name and will fail.
  Fix: Rename &lt;span class="k"&gt;in &lt;/span&gt;steps: add the new name, write to both, switch reads, &lt;span class="k"&gt;then &lt;/span&gt;remove the old name &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'migrax generate --zero-downtime'&lt;/span&gt; does this&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; Or deploy during a maintenance window.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is "no". If you just press Enter, Migrax treats the change as a drop and an add, and refuses as in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same answer as an option
&lt;/h3&gt;

&lt;p&gt;In a script, give the answer up front with &lt;code&gt;--rename table.old=new&lt;/code&gt;:&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;migrax generate &lt;span class="nt"&gt;--rename&lt;/span&gt; customers.name&lt;span class="o"&gt;=&lt;/span&gt;full_name
Created src/main/resources/db/migration/0003_rename_customers_name.sql:
  - rename column customers.name to full_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes exactly the same files as the "y" answer. Both runs above created byte-identical migrations. Separate several column renames with commas.&lt;/p&gt;

&lt;h3&gt;
  
  
  The migration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;db/migration/0003_rename_customers_name.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;-- Generated by Migrax 0.17.1 for postgresql&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;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db/migration/rollback/0003_rename_customers_name.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;-- Generated by Migrax 0.17.1 for postgresql&lt;/span&gt;
&lt;span class="c1"&gt;-- Rollback for 0003_rename_customers_name.sql. 'migrax rollback' runs it.&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;customers&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rename moves no data, so it is fast even on a large table. Apply it, and the names are still there:&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0003_rename_customers_name.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0003_rename_customers_name.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 3 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&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; id |  full_name   |      email
----+--------------+------------------
  1 | Ada Lovelace | ada@example.com
  2 | Alan Turing  | alan@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Migrax spots a rename
&lt;/h2&gt;

&lt;p&gt;It treats a dropped column and an added column in the same table as a possible rename when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;they are the same name in another style, like &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;createdAt&lt;/code&gt; (even when the type changed too), or&lt;/li&gt;
&lt;li&gt;the new column has the same type as the old one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rename that also changes the type becomes a &lt;code&gt;RENAME&lt;/code&gt; followed by an &lt;code&gt;ALTER&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renaming a table
&lt;/h2&gt;

&lt;p&gt;Now rename the &lt;code&gt;orders&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"purchase_orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PurchaseOrder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A table rename has one more catch. Hibernate puts the table name into the names of the foreign keys it creates, so renaming the table changes the key's name too. Without an answer, Migrax again sees a drop and refuses:&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;migrax generate
Possible rename: table orders to purchase_orders. To keep the data, rerun with &lt;span class="nt"&gt;--rename-table&lt;/span&gt; &lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;purchase_orders.
4 change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;postgresql:
  1. drop foreign key fk_orders_customer_id_54d4cc52 on orders  &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE]
     ALTER TABLE orders DROP CONSTRAINT fk_orders_customer_id_54d4cc52&lt;span class="p"&gt;;&lt;/span&gt;
  2. create table purchase_orders
     CREATE TABLE purchase_orders &lt;span class="o"&gt;(&lt;/span&gt;createdAt timestamp&lt;span class="o"&gt;(&lt;/span&gt;6&lt;span class="o"&gt;)&lt;/span&gt; with &lt;span class="nb"&gt;time &lt;/span&gt;zone, customer_id bigint NOT NULL, &lt;span class="nb"&gt;id &lt;/span&gt;bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL, total numeric&lt;span class="o"&gt;(&lt;/span&gt;12,2&lt;span class="o"&gt;)&lt;/span&gt; NOT NULL, CONSTRAINT purchase_orders_pkey PRIMARY KEY &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  3. drop table orders  &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE]
     DROP TABLE orders&lt;span class="p"&gt;;&lt;/span&gt;
  4. add foreign key fk_purchase_orders_cu_1ba47d9c on purchase_orders
     ALTER TABLE purchase_orders ADD CONSTRAINT fk_purchase_orders_cu_1ba47d9c FOREIGN KEY &lt;span class="o"&gt;(&lt;/span&gt;customer_id&lt;span class="o"&gt;)&lt;/span&gt; REFERENCES customers &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
error[MXE108]: 2 destructive change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; detected &lt;span class="o"&gt;(&lt;/span&gt;marked &lt;span class="o"&gt;[&lt;/span&gt;DESTRUCTIVE] above&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Review them, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax generate --allow-destructive'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; If a column or table was renamed, pass &lt;span class="nt"&gt;--rename&lt;/span&gt; table.old&lt;span class="o"&gt;=&lt;/span&gt;new or &lt;span class="nt"&gt;--rename-table&lt;/span&gt; &lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;new instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answer "y" in a terminal, or pass &lt;code&gt;--rename-table orders=purchase_orders&lt;/code&gt; (both write the same files again):&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;migrax generate
Did you rename table orders to purchase_orders? &lt;span class="o"&gt;[&lt;/span&gt;y/N] y
Created src/main/resources/db/migration/0004_rename_orders.sql:
  - rename table orders to purchase_orders
  - rename foreign key fk_orders_customer_id_54d4cc52 to fk_purchase_orders_cu_1ba47d9c on purchase_orders
Rollback script written to src/main/resources/db/migration/rollback.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The MX008 warning from before appears here too.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/0004_rename_orders.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;-- Generated by Migrax 0.17.1 for postgresql&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;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;purchase_orders&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;purchase_orders&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer_id_54d4cc52&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;fk_purchase_orders_cu_1ba47d9c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The foreign key keeps its columns and its target and only gets its new name. On PostgreSQL, CockroachDB, YugabyteDB and H2 that is a &lt;code&gt;RENAME CONSTRAINT&lt;/code&gt;, which is instant. Other databases can't rename a foreign key, so there Migrax drops the key and adds it again under the new name. That makes the database check every row once. Before Migrax 0.17.1, a table rename needed &lt;code&gt;--allow-destructive&lt;/code&gt; because of this key. Now the answer to the rename question is enough.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;migrax migrate&lt;/code&gt;, the orders still belong to the same customers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |  full_name   | total
----+--------------+--------
  1 | Ada Lovelace |  49.90
  2 | Ada Lovelace | 120.00
  3 | Alan Turing  |  15.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrax drift&lt;/code&gt; agrees that the database matches the entities:&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;migrax drift
No drift: the database matches the snapshot.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Undoing a rename
&lt;/h2&gt;

&lt;p&gt;The rollback script renames everything back, foreign key included:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/rollback/0004_rename_orders.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;purchase_orders&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_purchase_orders_cu_1ba47d9c&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer_id_54d4cc52&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;purchase_orders&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;orders&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;&lt;span class="nv"&gt;$ &lt;/span&gt;migrax rollback &lt;span class="nt"&gt;--yes&lt;/span&gt;
Rolling back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;, newest first:
  0004_rename_orders.sql
Rolling back migration &lt;span class="s1"&gt;'0004_rename_orders.sql'&lt;/span&gt;...
Rolled back migration &lt;span class="s1"&gt;'0004_rename_orders.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Rolled back 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rename moves no data, so undoing it loses none either. &lt;code&gt;migrax migrate&lt;/code&gt; applies it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rename while the old code still runs
&lt;/h2&gt;

&lt;p&gt;The MX008 warning is about deployment, not data. During a rolling deploy, instances of the old version keep running for a while. They still query &lt;code&gt;customers.name&lt;/code&gt; and fail as soon as the column is renamed. You have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deploy during a short maintenance window, so no old instance is left when the migration runs, or&lt;/li&gt;
&lt;li&gt;rename in steps: add the new column, write to both, switch reads, then drop the old one. &lt;code&gt;migrax generate --zero-downtime&lt;/code&gt; writes these steps for you (&lt;a href="https://migrax.org/guides/zero-downtime/" rel="noopener noreferrer"&gt;Zero-downtime changes&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrax spots likely renames and asks about them. It never drops data on a guess.&lt;/li&gt;
&lt;li&gt;In CI it refuses with MXE108 and prints the exact &lt;code&gt;--rename&lt;/code&gt; or &lt;code&gt;--rename-table&lt;/code&gt; option to use.&lt;/li&gt;
&lt;li&gt;A table rename also renames its foreign keys, in place where the database supports it.&lt;/li&gt;
&lt;li&gt;Every rename has a rollback script that renames it back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More in the guide: &lt;a href="https://migrax.org/guides/changing-entities/" rel="noopener noreferrer"&gt;Changing entities&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/renaming-columns-and-tables-without-losing-data/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>database</category>
      <category>postgres</category>
      <category>hibernate</category>
    </item>
    <item>
      <title>Generate SQL migrations from your JPA entities</title>
      <dc:creator>Feroz Shah</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:20:15 +0000</pubDate>
      <link>https://dev.to/fsmutimeer/generate-sql-migrations-from-your-jpa-entities-44ea</link>
      <guid>https://dev.to/fsmutimeer/generate-sql-migrations-from-your-jpa-entities-44ea</guid>
      <description>&lt;p&gt;Your JPA entities already describe the schema you want. Writing the same change again by hand as SQL is slow and easy to get wrong. Migrax compares your entities with the last state you committed and writes the SQL migration for you, along with a rollback script. You review the SQL, commit it, and apply it.&lt;/p&gt;

&lt;p&gt;This article walks through that loop on a small project: create the first tables, add a column, and catch a missing migration in CI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tested with:&lt;/strong&gt; Migrax 0.16.0, Hibernate 6.6.13, PostgreSQL 17. Every output below was copied from a real run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;Two entities, a customer and an order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"customers"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Id&lt;/span&gt;
  &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PurchaseOrder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Id&lt;/span&gt;
  &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@ManyToOne&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optional&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrax needs to know where the database is. In &lt;code&gt;src/main/resources/application.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;migrax.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jdbc:postgresql://localhost:5432/shop&lt;/span&gt;
&lt;span class="py"&gt;migrax.user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;shop&lt;/span&gt;
&lt;span class="py"&gt;migrax.password&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;shop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Spring Boot, Quarkus, Micronaut or Helidon project can skip this: Migrax reads the framework's own data source settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://migrax.org/getting-started/installation/" rel="noopener noreferrer"&gt;Install Migrax&lt;/a&gt;, then run &lt;code&gt;migrax init&lt;/code&gt; and &lt;code&gt;migrax doctor&lt;/code&gt; in the project folder. &lt;code&gt;init&lt;/code&gt; creates the migration folder; &lt;code&gt;doctor&lt;/code&gt; checks that the project compiles, finds the entities and reaches the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first migration
&lt;/h2&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;migrax generate
No snapshot yet: using the current database schema as the baseline.
Created src/main/resources/db/migration/0001_initial.sql:
  - create table customers
  - create table orders
  - add foreign key fk_orders_customer_id_54d4cc52 on orders
Rollback script written to src/main/resources/db/migration/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrax reads the entities through Hibernate's own mapping, so the SQL matches what Hibernate expects, column types and constraint names included:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/0001_initial.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;-- Generated by Migrax 0.16.0 for postgresql&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&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;150&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;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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="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="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;customers_pkey&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="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;uk_customers_email&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;TABLE&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;createdAt&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&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="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&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;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_pkey&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="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;fk_orders_customer_id_54d4cc52&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&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;customer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next to it is the rollback script. It says which data a rollback would delete:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db/migration/rollback/0001_initial.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;-- Generated by Migrax 0.16.0 for postgresql&lt;/span&gt;
&lt;span class="c1"&gt;-- Rollback for 0001_initial.sql. 'migrax rollback' runs it.&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: deletes data written to customers since the migration ran.&lt;/span&gt;
&lt;span class="c1"&gt;-- Note: deletes data written to orders since the migration ran.&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;DROP&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;fk_orders_customer_id_54d4cc52&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are plain SQL files. Read them, change them if you want, and apply them:&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;migrax migrate
Applying migration &lt;span class="s1"&gt;'0001_initial.sql'&lt;/span&gt;...
Successfully applied migration &lt;span class="s1"&gt;'0001_initial.sql'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Applied 1 migration&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 1 total &lt;span class="k"&gt;in &lt;/span&gt;src/main/resources/db/migration.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrax also wrote &lt;code&gt;.migrax/snapshot.json&lt;/code&gt;: the schema your entities describe right now. Commit it with the migration files. The next &lt;code&gt;generate&lt;/code&gt; compares against it, so it doesn't need a database to work out what changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a column
&lt;/h2&gt;

&lt;p&gt;Add a field to &lt;code&gt;Customer&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&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;&lt;span class="nv"&gt;$ &lt;/span&gt;migrax generate
Created src/main/resources/db/migration/0002_add_customers_phone.sql:
  - add column customers.phone
Rollback script written to src/main/resources/db/migration/rollback.
Review the SQL, &lt;span class="k"&gt;then &lt;/span&gt;run &lt;span class="s1"&gt;'migrax migrate'&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db/migration/0002_add_customers_phone.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="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&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;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file name describes the change. The rollback script is &lt;code&gt;ALTER TABLE customers DROP COLUMN phone;&lt;/code&gt;. Apply it with &lt;code&gt;migrax migrate&lt;/code&gt;, as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping entities and migrations in sync
&lt;/h2&gt;

&lt;p&gt;Someone will eventually change an entity and forget to generate the migration. Say a teammate renames the field &lt;code&gt;name&lt;/code&gt; to &lt;code&gt;fullName&lt;/code&gt; (column &lt;code&gt;full_name&lt;/code&gt;) and pushes only the Java change. &lt;code&gt;migrax check&lt;/code&gt; catches that without a database, so it fits in CI:&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;migrax check
2 entity change&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; have no migration:
  - drop column customers.name
  - add column customers.full_name
Run &lt;span class="s1"&gt;'migrax generate'&lt;/span&gt; and commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with code 2 when a migration is missing and 0 when everything matches:&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;migrax check
OK: entities match the snapshot.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Change an entity, run &lt;code&gt;migrax generate&lt;/code&gt;, read the SQL, commit, run &lt;code&gt;migrax migrate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every migration comes with a rollback script that says what data it would delete.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrax check&lt;/code&gt; in CI catches entity changes that have no migration.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://migrax.org/blog/generate-sql-migrations-from-jpa-entities/" rel="noopener noreferrer"&gt;migrax.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>database</category>
      <category>postgres</category>
      <category>hibernate</category>
    </item>
  </channel>
</rss>
