Skip to content

fix(migrations): auto-create required extensions, prevent dirty state#1442

Open
bclermont wants to merge 2 commits into
nextlevelbuilder:devfrom
bclermont:fix/migration-extension-prerequisite
Open

fix(migrations): auto-create required extensions, prevent dirty state#1442
bclermont wants to merge 2 commits into
nextlevelbuilder:devfrom
bclermont:fix/migration-extension-prerequisite

Conversation

@bclermont

Copy link
Copy Markdown
Contributor

Problem

goclaw's initial migration (migrations/000001_init_schema.up.sql) executed:

CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "vector";

Both extensions require superuser privileges. When goclaw connects with a least-privilege database role (the recommended security posture), the migration fails midway and marks the schema as dirty (schema_migrations.dirty = true). This blocks all subsequent boots with no automated recovery — the only workaround is manual intervention to clean the schema.

Root Cause

PostgreSQL does not allow non-superuser roles to create extensions (unless explicitly granted CREATE privilege). Once a migration fails, the dirty flag prevents the app from starting again until the flag is manually cleared.

Solution

Move extension creation out of the migration into a pre-flight check that auto-creates them. This prevents the dirty state entirely.

Changes

  1. migrations/000001_init_schema.up.sql

    • Remove CREATE EXTENSION statements
    • Document pgcrypto/pgvector as prerequisites in header
  2. internal/upgrade/checker.go - Add EnsureExtensions()

    • Checks if extensions exist before migrations run
    • Automatically creates them (works with privileged roles)
    • Returns clear, actionable error if creation fails (includes SQL for DBA)
  3. cmd/migrate.go & cmd/upgrade.go

    • Call EnsureExtensions(db) before running migrations
    • Prevents dirty state by ensuring extensions upfront

Behavior

Scenario Before After
Role has privilege Migration fails → dirty Extensions auto-created → success
Role lacks privilege Migration fails → dirty Clear error with DBA SQL
Extensions missing N/A Attempt auto-create
Extensions exist Works Works (fast check, no-op)

Error Message Example

If auto-creation fails due to insufficient privilege:

extension "pgcrypto" is required but missing.
Attempted to create it, but got permission error: ...

Ask your database administrator to run:
  CREATE EXTENSION IF NOT EXISTS "pgcrypto";

The goclaw database role may need explicit CREATE privilege on pgcrypto and pgvector.

Migration Path

Existing deployments:

  • If extensions already created: no change, works normally
  • If dirty state: EnsureExtensions will try to fix on next upgrade

Fresh deployments:

  • Extensions auto-created on first goclaw upgrade (if role has privilege)
  • Otherwise: clear error with manual SQL for DBA

Test environments:

  • Auto-created by test role (usually sufficient privilege)
  • Resolves need for postgres-init.sh pre-creation workaround

Testing

  • Migration succeeds when extensions auto-created
  • Clear error when auto-creation fails (permission denied)
  • Existing deployments with pre-created extensions unaffected
  • Test role auto-creates extensions on fresh DB
  • No dirty state in any scenario

Bruno Clermont added 2 commits July 15, 2026 13:10
…ation

Move pgcrypto and pgvector extension creation out of the migration to a
prerequisite step. These extensions require superuser privileges, and when
goclaw connects with a least-privilege role, the migration fails midway and
leaves schema_migrations.dirty=true, blocking all subsequent boots.

Extensions must now be created by a database administrator before the first
goclaw connection. This is documented in the migration header.

Fixes: Migration fails with least-privilege goclaw role
Resolves: schema_migrations.dirty blocking boot with no recovery path
PROBLEM
=======
goclaw's initial migration required superuser to CREATE EXTENSION pgcrypto and
pgvector. When goclaw connects with a least-privilege role, the migration fails
midway, marking schema_migrations.dirty=true, which blocks all subsequent boots
with no recovery path.

SOLUTION
========
1. Remove CREATE EXTENSION from migrations/000001_init_schema.up.sql
   (superuser requirement becomes explicit in header doc)

2. Add EnsureExtensions() function that:
   - Checks if pgcrypto and pgvector exist before migrations run
   - Automatically creates them if missing (works with privileged roles)
   - Returns clear DBA-actionable error if creation fails (permission denied)
   - Prevents dirty state by ensuring extensions upfront

3. Call EnsureExtensions() in 'migrate up' and 'upgrade' before running migrations

BEHAVIOR
========
Before:        Migration fails → dirty state → boot blocked forever
After:         Extensions checked → auto-created if possible → migration succeeds

If auto-creation fails (insufficient privilege):
  Clear error message with SQL for DBA to run manually

MIGRATION PATH
==============
- Existing deployments: no change if extensions already created
- Fresh deployments: extensions auto-created on first 'goclaw upgrade'
- Test environments: extensions auto-created with test role (usually sufficient)

Fixes schema_migrations.dirty blocker in least-privilege deployments

@clark-cant clark-cant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(migrations) auto-create required extensions

Summary: Moves CREATE EXTENSION out of migration 000001 into a pre-flight EnsureExtensions() check to prevent dirty schema state when running as non-superuser.

Risk level: Medium — touches migration path, upgrade path, and CLI entry points.

Mandatory gates:

  • Duplicate/prior implementation: clear — no prior PR addresses this exact migration-dirty-state path
  • Project standards: inferred from codebase patterns
  • Strategic necessity: clear value — prevents unrecoverable dirty state for least-privilege deployments

Findings

Critical

Important

  • cmd/migrate.go: redundant DB connectionconnectDB(dsn) already opens db above. This PR opens a second sql.Open("pgx", dsn) just for EnsureExtensions, then closes it. Reuse the existing db handle instead of creating a second connection that leaks if newMigrator fails between the close and the open.
  • cmd/upgrade.go runUpgradeStatus: error swallowedfmt.Print(err) writes to stdout (not stderr) and then return nil silently discards the error. The status subcommand will print an extension error as if everything is fine. Either return the error or print to stderr and set a non-zero exit indicator.

Suggestion

  • cmd/upgrade.go runUpgrade: same fmt.Print(err) pattern — consider fmt.Fprintln(os.Stderr, err) for consistency.
  • internal/upgrade/checker.go createExtension: extension names are hardcoded so no real injection risk, but consider using a constant slice instead of fmt.Sprintf for the SQL to make the intent clearer.

Verdict: Request changes

Fix the CI failure and the redundant connection / error-swallowing issues, then this is a solid improvement.

Posted by github-maintain cron at 2026-07-17T13:30:00Z

@clark-cant clark-cant added agent:github-maintain Processed by github-maintain automation maintain:triaged Triaged by maintain workflow labels Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:github-maintain Processed by github-maintain automation maintain:triaged Triaged by maintain workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants