Skip to content

GORM with a Reviewed Ptah Migration

Move schema changes out of a Go application's startup, adopt its existing GORM database with Ptah, and deploy a reviewed change before starting the new code.

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

A Go application calls GORM’s AutoMigrate when it starts. That puts schema changes on the application’s startup path. For the next release, we want to review the SQL, deploy it through Ptah, and let the application run with ordinary data access permissions.

The change is small: customer names can grow from 64 to 128 characters, and each customer gains a locale. The existing database already has two customer rows, so the handoff must preserve them.

This run used GORM 1.31.2, gorm.io/driver/postgres 1.6.3, Go 1.27.1, Ptah 0.10.0, and PostgreSQL 18.6. The supporting files include both application versions, generated and reviewed SQL, dependency pins, and measured results.

Capture the schema the application created

Section titled “Capture the schema the application created”

We ran the original application, including its AutoMigrate call, and inserted two customers. Ptah then inspected that database:

Terminal window
ptah schema inspect --db-url "$DB_URL" \
--schemas public --format sql > schema-v1.sql

The snapshot contains the customer table, its defaults and constraints, and the unique email index. Review that state before adopting it as the baseline.

Here, schema.sql will become the deployment source of truth, with GORM models mapped to it. This workflow makes schema changes explicit in SQL; it does not automatically translate future edits to GORM tags.

Record the existing state without recreating the table

Section titled “Record the existing state without recreating the table”

We generated the baseline against a separate empty database. EMPTY_URL and SHADOW_URL both name disposable databases, distinct from the existing target:

Terminal window
ptah migrations generate --db-url "$EMPTY_URL" \
--schema-file schema-v1.sql --schemas public \
--migrations-dir migrations --name customers_baseline \
--shadow-db "$SHADOW_URL"
ptah migrations hash --dir migrations
ptah migrations baseline --db-url "$DB_URL" \
--migrations-dir migrations --version 1790682270 \
--schemas public --shadow-db "$SHADOW_URL"

1790682270 is the version generated in this run; use the version from your own baseline file when generating a new one. Baseline verification replayed that file in shadow and compared it with the existing schema before recording it as applied. Both customer rows remained.

The updated GORM model gives Name a size of 128 and adds Locale with size 10, not null, and default en. We made those same changes in schema.sql, then asked Ptah to generate the migration:

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

The generated up file contains the locale addition and the name widening. The existing name column is already non-nullable, so the migration leaves that constraint in place. We accepted the generated SQL unchanged:

examples/expected/change.sql
ALTER TABLE "customers" ADD COLUMN "locale" VARCHAR(10) NOT NULL DEFAULT 'en';
ALTER TABLE "customers" ALTER COLUMN "name" TYPE VARCHAR(128);

The down file narrows the name before dropping locale. Both files include Ptah’s generated lock_timeout=3s and statement_timeout=30s directives. We reviewed the pair, then verified the same bytes against newly initialized databases before deploying them. The supporting files preserve both the generated output and the accepted migration directory.

Record the checksum and lint with a disposable database so Ptah can read the prior schema:

Terminal window
ptah migrations hash --dir migrations
ptah migrations lint --dir migrations \
--dev-url "$SHADOW_URL" --latest 1 --fail-on error

The check returned zero and reported PG301P as an informational finding: this varchar widening is a PostgreSQL catalog change, with no table rewrite or index rebuild. That finding does not promise zero lock acquisition or establish application latency under load.

Deploy the schema before starting the new application

Section titled “Deploy the schema before starting the new application”
Terminal window
ptah migrations up --db-url "$DB_URL" \
--migrations-dir migrations --verify-sum
ptah schema compare --db-url "$DB_URL" \
--schema-file schema.sql --schemas public --exit-code

Ptah applied one pending migration and the comparison passed. Both existing customers received locale en.

The updated application’s startup no longer calls AutoMigrate. We ran it with a role allowed to read and write customer data and use the ID sequence, but without CREATE on public. It inserted a customer with a 97-character name and read all three customers successfully. A separate CREATE TABLE attempt by that same role failed with permission denied for schema public.

This verifies the separation directly: the migration credentials change the schema; the running application uses the resulting tables.

Test rollback with data that uses the new limit

Section titled “Test rollback with data that uses the new limit”

A fresh database replayed both reviewed migrations and matched the desired schema. We then ran the new application there, inserting the same long name, and attempted to roll back to the baseline.

Treat that as a deployment decision before release: once the application stores longer names, rolling back the column width needs a data policy. Silently truncating customer names is not part of this example.

GORM still maps and queries the application data. Ptah now provides the reviewed migration, recorded baseline, lint findings, and schema checks that run before the application starts. Keep the SQL schema and GORM model in the same change, and test the client against the deployed result.

Put Ptah to work

Example files