Skip to content

Moving a Go Service from golang-migrate to Ptah

Import existing SQL migration pairs, keep the old history table intact, and verify a Go queue worker against the next schema change deployed by Ptah.

Ptah helps you plan, review, and apply database migrations. Try in your browser

The SQL files look familiar: a numbered up file and a matching down file. That makes a golang-migrate history straightforward to import into Ptah. The database history table needs more care. Both tools use the name schema_migrations by default, but they expect different columns.

We’ll move a small job queue to Ptah, preserve the original history table, and add an attempt counter. A Go worker will then complete an existing job using the new column.

The run used golang-migrate 4.20.1, Ptah 0.9.0, PostgreSQL 18.6, Go 1.27.1, and pgx 5.11.0. The supporting files include the original migrations, worker, pinned dependencies, and recorded results.

Import the pairs that produced the database

Section titled “Import the pairs that produced the database”

golang-migrate applied version 1 to create public.jobs and version 2 to index unfinished jobs by queue. We inserted two pending email jobs. The source tool reported version 2, and its history row had dirty = false.

Freeze further runs through golang-migrate during the handoff, then import:

Terminal window
ptah migrations import --from golang-migrate \
--source-dir source --migrations-dir migrations

Ptah kept versions 1 and 2, imported their up and down SQL, and wrote ptah.sum. Importing files does not adopt the database yet.

On a disposable clone, we tried reading the original table with Ptah’s default settings. The status command returned exit code 2:

examples/expected/history-collision.txt
error: error getting migration status: failed to get migration revisions: failed to query migration revisions: ERROR: column "description" does not exist (SQLSTATE 42703)

golang-migrate records the current version and dirty flag. Ptah expects per-migration revision records. Reusing the table name does not convert its contents into the other format.

We created a separate ptah_history schema and kept the old table in public. Use the same metadata schema for every Ptah history operation:

Terminal window
ptah migrations baseline --db-url "$DB_URL" \
--migrations-dir migrations --version 2 \
--migrations-schema ptah_history --schemas public \
--shadow-db "$SHADOW_URL"

SHADOW_URL names a different, disposable database. Ptah replayed versions 1 and 2 there, checked the application schema against the existing database, and recorded both versions in ptah_history.schema_migrations.

The original public.schema_migrations table stayed at version 2, clean. The two queued jobs stayed in place. Ptah excludes schema_migrations from its application-schema inspection; the schema comparison does not translate or validate the old engine’s history for us.

The desired schema adds attempts integer NOT NULL DEFAULT 0. Existing jobs begin at zero; the worker increments the counter when it processes one.

Terminal window
ptah migrations generate --db-url "$DB_URL" \
--schema-file schema.sql --schemas public \
--migrations-dir migrations --name job_attempts \
--shadow-db "$SHADOW_URL"

The generated change is:

examples/expected/new-change.sql
ALTER TABLE "public"."jobs" ADD COLUMN "attempts" integer NOT NULL DEFAULT 0;

Generation passed the shadow apply/rollback/reapply check. After reviewing the pair, update the checksum and run the new version through lint:

Terminal window
ptah migrations hash --dir migrations
ptah migrations lint --dir migrations --dialect postgres --latest 1 --fail-on error
ptah migrations up --db-url "$DB_URL" \
--migrations-dir migrations --migrations-schema ptah_history --verify-sum
ptah schema compare --db-url "$DB_URL" \
--schema-file schema.sql --schemas public --exit-code

Lint reported no findings. The existing database applied one migration and matched the desired schema. The native Go worker then updated job 1 and read its result through pgx:

examples/expected/worker.txt
job=1 payload=receipt:42 attempts=1 completed=true

Job 2 remained pending with zero attempts. The original history still reported 2, false; Ptah’s separate table held three applied versions.

Check a fresh installation and the imported down file

Section titled “Check a fresh installation and the imported down file”

We created ptah_history on another empty database, replayed the complete directory, and compared it with the same desired schema:

Terminal window
ptah migrations up --db-url "$FRESH_URL" \
--migrations-dir migrations --migrations-schema ptah_history --verify-sum
ptah schema compare --db-url "$FRESH_URL" \
--schema-file schema.sql --schemas public --exit-code

All three versions applied. Both existing and fresh databases had zero pending migrations and matching application schemas.

On the disposable fresh database, rolling back to version 1 removed the attempt counter and ran the imported index rollback:

Terminal window
ptah migrations down --db-url "$FRESH_URL" \
--migrations-dir migrations --migrations-schema ptah_history \
--target 1 --verify-sum --confirm

The jobs table remained; the index and counter were absent. Reapplying the directory restored the desired schema. Dropping the counter would discard its values on a populated database, so this is a schema rollback test, not a recovery plan for processed jobs.

The files moved cleanly. The history required an explicit boundary. Keep the metadata schema setting with the deployment configuration, commit the imported directory and desired schema, and send future changes through Ptah.

Put Ptah to work

Example files