<?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>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>
