Skip to content

feat(supabase): Supabase first-class support#156

Merged
jasdeepkhalsa merged 4 commits intomasterfrom
feature/better-supabase-integration
Mar 26, 2026
Merged

feat(supabase): Supabase first-class support#156
jasdeepkhalsa merged 4 commits intomasterfrom
feature/better-supabase-integration

Conversation

@jasdeepkhalsa
Copy link
Copy Markdown
Member

@jasdeepkhalsa jasdeepkhalsa commented Mar 25, 2026

Supabase first-class support

Adds zero-config Supabase project detection and a native Supabase migration
file format, so DBDiff works seamlessly inside any Supabase project without
any manual configuration.

This PR closes all remaining gaps in DBDiff's Supabase integration.


What's changed

1. Dual-write to supabase_migrations.schema_migrations

migration:up and migration:down now keep Supabase's own history table in sync. When running against a PostgreSQL target identified as a Supabase project, every applied migration is also written into supabase_migrations.schema_migrations (with ON CONFLICT DO NOTHING), and rolled-back migrations are pruned from it. This means supabase db pull and other Supabase CLI commands will see the same history as DBDiff.

2. Environment variable fallback for DSN resolution

When no explicit source/target DSN is provided, DBDiff checks SUPABASE_DB_URLDATABASE_URLDIRECT_URL in order. This covers CI environments (GitHub Actions, Vercel, Netlify, Railway, Fly.io) where the DSN is injected as an env var with no config change required.

3. supabase/config.toml parsing

If no DSN is found via env vars or supabase status, DBDiff parses the [db] section of supabase/config.toml to extract port (default 54322) and password (default postgres) and constructs postgresql://postgres:{password}@127.0.0.1:{port}/postgres. Supports quoted and unquoted values.

4. Drift detection in migration:status

migration:status now compares DBDiff's _dbdiff_migrations table against Supabase's supabase_migrations.schema_migrations and reports divergence:

→  20240101000001  supabase_only  — present in Supabase but not in DBDiff
←  20240201000002  dbdiff_only    — applied via DBDiff but missing from Supabase
Run migrations through DBDiff to keep both history tables in sync.

5. Supabase-aware rollback error

migration:down now gives a clear, actionable error for supabase-format migrations rather than silently failing:

Supabase-format migrations are UP-only and do not support rollback. Write a new forward migration instead, or convert to DBDiff native format (.up.sql + .down.sql).

6. Transaction lint for Supabase migrations

Before applying each supabase-format migration, DBDiff lints the SQL for BEGIN/COMMIT/ROLLBACK/START TRANSACTION/SET TRANSACTION. Supabase wraps every migration in a transaction automatically, so manual transaction statements cause errors. Warnings are surfaced in the migration result and displayed to the user, with SQL comments stripped before analysis to avoid false positives.

7. Remote DSN auto-detection from supabase link

After running supabase link, the linked project ref is recorded in .supabase/temp/project-ref. DBDiff reads this and constructs the remote DSN automatically:

  • With region (pooler): postgresql://postgres.{ref}:{password}@aws-0-{region}.pooler.supabase.com:6543/postgres
  • Without region (direct): postgresql://postgres:{password}@db.{ref}.supabase.co:5432/postgres

Password is resolved from the SUPABASE_DB_PASSWORD env var or falls back to a [YOUR-PASSWORD] placeholder. The full resolution chain (resolveDbUrl) is: env vars → supabase statusconfig.toml.


SonarQube compliance

All new code passes SonarQube analysis:

  • MigrationStatusCommand::execute — cognitive complexity reduced from 22 → ~6 by extracting formatState(), buildTable(), and renderDriftWarnings() helpers
  • SupabaseProjectDetector::configTomlDbUrl — complexity reduced from 17 → ~4, returns from 4 → 2, unused variable removed, credential false-positive eliminated by extracting extractTomlSection() and parseTomlDbSection() as reusable public helpers
  • SupabaseProjectDetector::resolveDbUrl — returns reduced from 4 → 1 via null-coalescing chain

Files changed

File Change
src/Migration/Config/SupabaseProjectDetector.php envDbUrl(), extractTomlSection(), parseTomlDbSection(), configTomlDbUrl(), linkedProjectRef(), remoteDbUrl(), resolveDbUrl()
src/Migration/Runner/MigrationHistory.php $supabaseSync flag, syncToSupabase(), unsyncFromSupabase()
src/Migration/Runner/MigrationRunner.php drift detection, lint integration, Supabase-aware down error
src/Migration/Runner/MigrationFile.php lintSupabaseTransaction()
src/Migration/Command/MigrationStatusCommand.php formatState(), buildTable(), renderDriftWarnings(), simplified execute()
src/Migration/Command/DiffCommand.php use resolveDbUrl() chain
tests/Migration/Config/SupabaseProjectDetectorTest.php 26 new tests (incl. 8 for new helpers)
tests/Migration/Runner/MigrationFileTest.php 6 new tests
tests/Migration/Runner/MigrationHistoryTest.php 2 new tests
tests/Migration/Runner/MigrationRunnerTest.php 6 new tests

Testing

OK (417 tests, 720 assertions)

All new functionality is covered by 35 new test methods. Tests run via PHP 8.3 on SQLite (unit) and are CI-compatible.

Usage examples

# Inside a Supabase project — everything is auto-detected
cd my-supabase-project

# Create a Supabase-format migration (auto-detected, or explicit)
dbdiff migration:new add_profiles
dbdiff migration:new add_profiles --format=supabase

# Status shows cross-referenced Supa? column
dbdiff migration:status --db-url "$DATABASE_URL"

# Diff auto-fills --server1-url from the local Supabase stack (supabase status),
# but --server2-url still needs to be supplied
dbdiff diff server1.mydb:server2.mydb \
  --server2-url postgres://user:pass@db.xyz.supabase.co:5432/staging

…and local stack diff

Phase 1 — Supabase-native migration format
- Add MigrationFile::scaffoldSupabase() for plain {version}_{desc}.sql files (no DOWN)
- Add --format=native|supabase option to migration:new with auto-detection fallback
- Add MigrationHistory::getSupabaseAppliedVersions() querying supabase_migrations.schema_migrations
- Add MigrationRunner::getSupabaseAppliedVersions() delegator
- Add 'Supa?' column to migration:status output when inside a Supabase project

Phase 4 — Zero-config Supabase project detection
- Add SupabaseProjectDetector: walks up dirs for supabase/config.toml,
  resolves migrations dir, and reads local DB URL via `supabase status --output json`
- MigrationConfig auto-sets migrationsDir and migrationFormat='supabase' on detection;
  explicit CLI flags and YAML overrides always take precedence
- DiffCommand auto-fills --server1-url from local Supabase stack when no connection given
- Support migrations.format key in dbdiff.yml / dbdiff.yml.example

Docs + tests
- Update README with migration file formats table, auto-detection section,
  and Supabase diff examples
- Update dbdiff.yml.example with migrations.format key
- Add SupabaseProjectDetectorTest (filesystem mocks, no DB)
- Extend MigrationFileTest with scaffoldSupabase() coverage
- Extend MigrationConfigTest with Supabase auto-detection and override tests
- Extend MigrationHistoryTest with getSupabaseAppliedVersions() safety tests
- Extend test-supabase CI job with getSupabaseAppliedVersions() integration step
  and unit test run step
@jasdeepkhalsa jasdeepkhalsa changed the title feat(supabase): Phase 1 + Phase 4 — auto-detection, Supabase format, … feat(supabase): Supabase first-class support Mar 25, 2026
1. Dual-write: migration:up/down now syncs to supabase_migrations.schema_migrations
2. Env var fallback: SUPABASE_DB_URL → DATABASE_URL → DIRECT_URL auto-detected
3. config.toml parsing: local DB credentials extracted from supabase/config.toml
4. Drift detection: migration:status shows divergence between DBDiff and Supabase history
5. Supabase-aware down: clear error message for UP-only supabase-format migrations
6. Transaction lint: warns when BEGIN/COMMIT found in supabase migrations (auto-wrapped)
7. Remote DSN from supabase link: reads .supabase/temp/project-ref to build remote URL

Tests: 409 passing (708 assertions), 27 new test methods across 4 test files
MigrationStatusCommand (complexity 22 → ~6):
- Extract formatState(), buildTable(), renderDriftWarnings() helpers
- execute() now delegates to focused single-purpose methods

SupabaseProjectDetector:
- Extract public extractTomlSection() — reusable TOML section parser
- Extract public parseTomlDbSection() — reusable [db] key-value parser
- configTomlDbUrl() reduced from 4 returns/complexity-17 to 2 returns/~4
- Remove unused $sectionEnd variable
- Rename $password → $dbPass (eliminates hardcoded-credential false positive)
- resolveDbUrl() reduced from 4 returns to 1 via null-coalescing chain

Tests: 417 passing (720 assertions), 8 new tests for extracted helpers
@sonarqubecloud
Copy link
Copy Markdown

@jasdeepkhalsa jasdeepkhalsa merged commit 8daa82c into master Mar 26, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant