#3795·atlas

migrate status ignores baseline/allow-dirty when revisions table exists but is empty — wedges Atlas Operator on non-clean databases (e.g. GCP Cloud SQL)

Author: TheLastAirsickLowlanderCreated Sep 4, 2026Updated Sep 4, 2026

Environment

  • atlas v1.3.1-629a976-canary (bundled in arigaio/atlas-operator:0.7.39); also reproduced on v1.2.1 and confirmed by inspection on master
  • Target: PostgreSQL 18 on GCP Cloud SQL (any DB with a provider-managed schema, e.g. google_vacuum_mgmt)
  • Driver path: Atlas Kubernetes Operator AtlasMigration with spec.baseline set

Summary

atlas migrate status fails with sql/migrate: connected database is not clean: found schema "google_vacuum_mgmt". baseline version or allow-dirty is required even when baseline is configured, whenever the revisions table exists but contains zero rows. atlas migrate apply succeeds on the identical state, because only apply threads baseline into the executor. Because the Atlas Kubernetes Operator runs migrate status before every migrate apply, this state permanently wedges the operator: status fails → apply never runs → backoff → Stalled.

Root cause

  1. cmd/atlas/internal/cmdlog/cmdlog.goStatusReporter.Report: when the revisions table exists, it builds the executor with no options:

    go
    ex, err := migrate.NewExecutor(r.Client.Driver, r.Dir, rrw)   // no WithBaselineVersion / WithAllowDirty

    then calls ex.Pending(ctx).

  2. sql/migrate/migrate.goExecutor.Pending: with len(revs) == 0, it runs CheckClean; a NotCleanError is only tolerated when baselineVer != "" || allowDirty — neither is ever set on the status executor.

  3. cmd/atlas/internal/cmdapi/migrate.gomigrateStatusCmd doesn't even register --baseline/--allow-dirty flags (contrast migrateApplyCmd), so maySetFlag(cmd, flagBaseline, env.Migration.Baseline) silently no-ops (f == nil) and an env-level migration { baseline = ... } is dropped for status.

So on any database that isn't clean — notably every Cloud SQL database, which always carries google_vacuum_mgmt — status is strictly weaker than apply.

How the empty-but-present revisions table arises

migrateApplyRun calls mrrw.Migrate(ctx) (creates the revisions table) before ex.Pending(ctx) writes the baseline revision. Any interruption between those steps (operator pod restart/OOM, crash, connectivity loss) leaves exactly this state. External state loss (manual cleanup, row deletion) produces it too. The next migrate status then fails forever, and since apply is never reached, the system cannot self-heal.

Minimal repro

bash
docker run -d --name pg -e POSTGRES_PASSWORD=pass -p 5432:5432 postgres:18-alpine
# provider-managed schema Atlas doesn't control:
psql "postgres://postgres:pass@localhost:5432/postgres" -c 'CREATE SCHEMA google_vacuum_mgmt'

mkdir migrations
printf 'CREATE TABLE t1 (id int);\n' > migrations/20260101000000_baseline.sql
atlas migrate hash --dir file://migrations

cat > atlas.hcl <<'EOF'
env "k8s" {
  url = "postgres://postgres:pass@localhost:5432/postgres?sslmode=disable"
  migration {
    dir      = "file://migrations"
    baseline = "20260101000000"
  }
}
EOF

atlas migrate apply --env k8s    # OK — baseline honored

# Simulate crash-between-table-create-and-first-row, or external state loss:
psql "postgres://postgres:pass@localhost:5432/postgres" \
  -c 'DELETE FROM atlas_schema_revisions.atlas_schema_revisions'

atlas migrate status --env k8s
# Error: sql/migrate: connected database is not clean: found schema
# "google_vacuum_mgmt". baseline version or allow-dirty is required   ← BUG

atlas migrate apply --env k8s    # OK again — apply tolerates the same state

Expected behavior

migrate status honors baseline/allow-dirty exactly like migrate apply — or at minimum never fails on a state where apply succeeds.

Suggested fix

Thread the options through: register --baseline/--allow-dirty on migrateStatusCmd, add BaselineVersion/AllowDirty fields to cmdlog.StatusReporter, and pass migrate.WithBaselineVersion(...)/migrate.WithAllowDirty(...) into the NewExecutor call in Report.

Impact / workaround

Any Atlas Operator deployment against non-clean databases (all GCP Cloud SQL instances; likely Azure/AWS managed Postgres equivalents) can be permanently wedged by a single interrupted first apply. Manual repair is required: atlas migrate apply --baseline <version> once per database (which also demonstrates the asymmetry — apply works, status doesn't). Affected operator-side flow: migrationRun.migrateStatusapplyChanges in internal/controller/atlasmigration_controller.go.

Related: #3609 (--exclude not working for schema apply, same google_vacuum_mgmt provider-schema class of problem).