Skip to content
PtahDocs
v0.8.0
Page type: tutorial

Migrate from Liquibase

Convert a Liquibase changelog, and learn which of your changesets carry SQL and which do not.

What decides whether a Liquibase changeset converts is not the file format. It is what the changeset carries.

A changeset that carries SQL converts, whether you wrote it as formatted SQL or as <sql> inside XML. A changeset that carries a typed change — <createTable>, <addColumn>, and the rest of Liquibase’s database-independent vocabulary — has no SQL to carry over, and Ptah refuses it by name rather than generating SQL it would have to guess the dialect for.

Carrying SQL is necessary and not sufficient. context, contexts, labels and preConditions decide at run time whether a changeset applies, and a migration directory cannot express that, so a changeset carrying one is refused as well. Importing it would turn a conditional history into an unconditional one, which is a worse outcome than a refusal.

This page converts what converts and reads both refusals.

  • A ptah binary on your PATH. Install Ptah if necessary.
  • A terminal and about eight minutes.

No database server, Docker, or Go toolchain is required.

Terminal window
mkdir -p ptah-from-liquibase/legacy
cd ptah-from-liquibase
cat > legacy/001-users.sql <<'SQL'
--liquibase formatted sql
--changeset alice:1
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
);
--rollback DROP TABLE users;
--changeset alice:2
CREATE UNIQUE INDEX users_email_idx ON users (email);
--rollback DROP INDEX users_email_idx;
SQL
Terminal window
ptah migrations import --source-dir ./legacy --migrations-dir ./migrations

Expected output on standard output:

Wrote 4 migration file(s) to ./migrations
Wrote ./migrations/ptah.sum
0000000001_alice_1.up.sql
0000000001_alice_1.down.sql
0000000002_alice_2.up.sql
0000000002_alice_2.down.sql

Each changeset became a migration, named from its author and id. The --rollback line became the down file, which is what it already was.

Terminal window
mkdir -p xml/legacy
cat > xml/legacy/changelog.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
<changeSet id="1" author="alice">
<sql>CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);</sql>
<rollback>DROP TABLE users;</rollback>
</changeSet>
</databaseChangeLog>
XML
Terminal window
ptah migrations import --from liquibase --source-dir ./xml/legacy --migrations-dir ./xml/migrations

Expected output on standard output:

Wrote 2 migration file(s) to ./xml/migrations
Wrote ./xml/migrations/ptah.sum
0000000001_alice_1.up.sql
0000000001_alice_1.down.sql

XML is read. Nothing about the format stops the conversion.

A changeset carrying a typed change is refused, by name

Section titled “A changeset carrying a typed change is refused, by name”
Terminal window
mkdir -p typed/legacy
cat > typed/legacy/changelog.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
<changeSet id="1" author="alice">
<createTable tableName="users">
<column name="id" type="int"/>
</createTable>
</changeSet>
</databaseChangeLog>
XML
Terminal window
ptah migrations import --from liquibase --source-dir ./typed/legacy --migrations-dir ./typed/migrations

Expected output on standard error:

error: parse liquibase source: liquibase changeset alice_1 in "changelog.xml" uses <createTable>, which is not SQL text and which Ptah does not generate per dialect; rewrite it as a `sql` change or import it by hand

The message names the changeset, the file and the element, so a changelog with one offender tells you which one on the first run. Work through them by rewriting each as a sql change in Liquibase first, where you can still run liquibase update-sql to see what it would have generated, and convert once they all carry SQL.

That is also the honest reason for the refusal: a typed change is database-independent, and turning it into SQL means choosing a dialect. Ptah will not choose one on your behalf inside an import.

A changeset carrying a selector is refused too

Section titled “A changeset carrying a selector is refused too”

The changeset below carries SQL, so the rule above stops short of deciding it. context="staging" is what makes the difference.

Terminal window
mkdir -p conditional/legacy
cat > conditional/legacy/changelog.xml <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog">
<changeSet id="1" author="alice" context="staging">
<sql>CREATE TABLE users (id INTEGER PRIMARY KEY);</sql>
</changeSet>
</databaseChangeLog>
XML
Terminal window
ptah migrations import --from liquibase --source-dir ./conditional/legacy --migrations-dir ./conditional/migrations

Expected output on standard error:

error: parse liquibase source: liquibase changeset alice_1 in "changelog.xml" is conditional on context; a migration directory has no equivalent, so importing it would turn a conditional history into an unconditional one -- split the changelog or import it by hand

contexts, labels and preConditions are refused the same way. Split the changelog per environment in Liquibase first, where the selector still means something, and convert each result separately.

Terminal window
ptah migrations validate --dir ./migrations

Expected output on standard output:

OK: migrations directory matches ptah.sum
Terminal window
ptah migrations up --db-url sqlite://app.db --migrations-dir ./migrations

Expected output includes, on standard output:

Total migrations: 2
Pending migrations: 2

Progress records on standard error carry timestamps and correlation IDs, so this page does not copy those volatile fields.

Terminal window
ptah migrations status --db-url sqlite://app.db --migrations-dir ./migrations

Expected output includes, on standard output:

Current Version: 2
Total Migrations: 2
Applied Migrations: 2
Pending Migrations: 0
Terminal window
cd ..
rm -rf ptah-from-liquibase

Contexts, labels and preconditions have no destination in a migration directory, which is why a changeset carrying one is refused rather than imported without it. Deciding what they meant is work that belongs in Liquibase: a context is usually the reason a changeset ran in one environment and not another, and once the changelog is split along that line each half converts.

If a database already has Liquibase’s DATABASECHANGELOG table, do not run ptah migrations up against it. Record the history as already applied first: see Adopt an existing database.

Import an existing migration directory covers the other source tools and the format table.