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:
ptah migrations import --from golang-migrate \ --source-dir source --migrations-dir migrationsPtah kept versions 1 and 2, imported their up and down SQL, and wrote
ptah.sum. Importing files does not adopt the database yet.
Give Ptah its own history
Section titled “Give Ptah its own history”On a disposable clone, we tried reading the original table with Ptah’s default settings. The status command returned exit code 2:
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:
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.
Generate an attempt counter
Section titled “Generate an attempt counter”The desired schema adds attempts integer NOT NULL DEFAULT 0. Existing jobs
begin at zero; the worker increments the counter when it processes one.
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:
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:
ptah migrations hash --dir migrationsptah migrations lint --dir migrations --dialect postgres --latest 1 --fail-on errorptah migrations up --db-url "$DB_URL" \ --migrations-dir migrations --migrations-schema ptah_history --verify-sumptah schema compare --db-url "$DB_URL" \ --schema-file schema.sql --schemas public --exit-codeLint 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:
job=1 payload=receipt:42 attempts=1 completed=trueJob 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:
ptah migrations up --db-url "$FRESH_URL" \ --migrations-dir migrations --migrations-schema ptah_history --verify-sumptah schema compare --db-url "$FRESH_URL" \ --schema-file schema.sql --schemas public --exit-codeAll 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:
ptah migrations down --db-url "$FRESH_URL" \ --migrations-dir migrations --migrations-schema ptah_history \ --target 1 --verify-sum --confirmThe 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
- expected/history-collision.txt
- expected/new-change.sql
- expected/worker.txt
- measured/application-state.txt
- measured/baseline-final.txt
- measured/commands.json
- measured/compare-fresh.txt
- measured/compare-live.txt
- measured/down-fresh.txt
- measured/generate-final.txt
- measured/hash.txt
- measured/history-collision.txt
- measured/import.txt
- measured/lint.txt
- measured/reapply-fresh.txt
- measured/recompare-fresh.txt
- measured/rollback-state.txt
- measured/seed.txt
- measured/source-state.txt
- measured/source-up.txt
- measured/source-version.txt
- measured/status-fresh.txt
- measured/status-live.txt
- measured/up-fresh.txt
- measured/up-live.txt
- measured/worker.txt
- migrations/0000000001_jobs.down.sql
- migrations/0000000001_jobs.up.sql
- migrations/0000000002_queue_index.down.sql
- migrations/0000000002_queue_index.up.sql
- migrations/1790476426_job_attempts.down.sql
- migrations/1790476426_job_attempts.up.sql
- migrations/ptah.sum
- README.md
- schema.sql
- seed.sql
- source/000001_jobs.down.sql
- source/000001_jobs.up.sql
- source/000002_queue_index.down.sql
- source/000002_queue_index.up.sql
- verified.json
- worker/go.mod
- worker/go.sum
- worker/main.go