From 217ff8192082d95be57cfedb5e8cfcdc7a5e1407 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Wed, 18 Feb 2026 16:51:25 -0700 Subject: [PATCH 1/3] feat(db): add --use-shadow-db flag to test db command Runs pgTAP tests against an ephemeral shadow database built from migrations, keeping the local dev database untouched. Reuses the existing CreateShadowDatabase/MigrateShadowDatabase machinery from db diff. Uses host networking so pg_prove can reach the shadow container via 127.0.0.1:. --- cmd/db.go | 4 +++- cmd/test.go | 1 + internal/db/test/test.go | 34 ++++++++++++++++++++++++++++++-- internal/db/test/test_test.go | 8 ++++---- pkg/config/templates/config.toml | 2 +- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cmd/db.go b/cmd/db.go index 409ef4238..5c625af9c 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -238,7 +238,8 @@ var ( Use: "test [path] ...", Short: "Tests local database with pgTAP", RunE: func(cmd *cobra.Command, args []string) error { - return test.Run(cmd.Context(), args, flags.DbConfig, afero.NewOsFs()) + useShadow, _ := cmd.Flags().GetBool("use-shadow-db") + return test.Run(cmd.Context(), args, flags.DbConfig, useShadow, afero.NewOsFs()) }, } ) @@ -349,6 +350,7 @@ func init() { testFlags.String("db-url", "", "Tests the database specified by the connection string (must be percent-encoded).") testFlags.Bool("linked", false, "Runs pgTAP tests on the linked project.") testFlags.Bool("local", true, "Runs pgTAP tests on the local database.") + testFlags.Bool("use-shadow-db", false, "Creates a temporary database from migrations for running tests in isolation.") dbTestCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local") rootCmd.AddCommand(dbCmd) } diff --git a/cmd/test.go b/cmd/test.go index 55e052066..d838743bf 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -41,6 +41,7 @@ func init() { dbFlags.String("db-url", "", "Tests the database specified by the connection string (must be percent-encoded).") dbFlags.Bool("linked", false, "Runs pgTAP tests on the linked project.") dbFlags.Bool("local", true, "Runs pgTAP tests on the local database.") + dbFlags.Bool("use-shadow-db", false, "Creates a temporary database from migrations for running tests in isolation.") testDbCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local") testCmd.AddCommand(testDbCmd) // Build new command diff --git a/internal/db/test/test.go b/internal/db/test/test.go index 2852ee943..d45694cdd 100644 --- a/internal/db/test/test.go +++ b/internal/db/test/test.go @@ -16,6 +16,8 @@ import ( "github.com/jackc/pgx/v4" "github.com/spf13/afero" "github.com/spf13/viper" + "github.com/supabase/cli/internal/db/diff" + "github.com/supabase/cli/internal/db/start" "github.com/supabase/cli/internal/utils" cliConfig "github.com/supabase/cli/pkg/config" ) @@ -25,7 +27,31 @@ const ( DISABLE_PGTAP = "drop extension if exists pgtap" ) -func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { +func Run(ctx context.Context, testFiles []string, config pgconn.Config, useShadowDb bool, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error { + // Create and migrate shadow database if requested + if useShadowDb { + fmt.Fprintln(os.Stderr, "Creating shadow database for testing...") + shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort) + if err != nil { + return err + } + defer utils.DockerRemove(shadow) + if err := start.WaitForHealthyService(ctx, utils.Config.Db.HealthTimeout, shadow); err != nil { + return err + } + if err := diff.MigrateShadowDatabase(ctx, shadow, fsys, options...); err != nil { + return err + } + // Override config to point at shadow DB + config = pgconn.Config{ + Host: utils.Config.Hostname, + Port: utils.Config.Db.ShadowPort, + User: "postgres", + Password: utils.Config.Db.Password, + Database: "postgres", + } + fmt.Fprintln(os.Stderr, "Shadow database ready. Running tests...") + } // Build test command if len(testFiles) == 0 { absTestsDir, err := filepath.Abs(utils.DbTestsDir) @@ -79,7 +105,11 @@ func Run(ctx context.Context, testFiles []string, config pgconn.Config, fsys afe // Use custom network when connecting to local database // disable selinux via security-opt to allow pg-tap to work properly hostConfig := container.HostConfig{Binds: binds, SecurityOpt: []string{"label:disable"}} - if utils.IsLocalDatabase(config) { + if useShadowDb { + // Shadow container has no Docker DNS alias; use host networking + // so pg_prove reaches it via 127.0.0.1: + hostConfig.NetworkMode = network.NetworkHost + } else if utils.IsLocalDatabase(config) { config.Host = utils.DbAliases[0] config.Port = 5432 } else { diff --git a/internal/db/test/test_test.go b/internal/db/test/test_test.go index 063f9bcd1..547c6b671 100644 --- a/internal/db/test/test_test.go +++ b/internal/db/test/test_test.go @@ -44,7 +44,7 @@ func TestRunCommand(t *testing.T) { apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.Images.PgProve), containerId) require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "Result: SUCCESS")) // Run test - err := Run(context.Background(), []string{"nested"}, dbConfig, fsys, conn.Intercept) + err := Run(context.Background(), []string{"nested"}, dbConfig, false, fsys, conn.Intercept) // Check error assert.NoError(t, err) }) @@ -54,7 +54,7 @@ func TestRunCommand(t *testing.T) { fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), nil, dbConfig, fsys) + err := Run(context.Background(), nil, dbConfig, false, fsys) // Check error assert.ErrorContains(t, err, "failed to connect to postgres") }) @@ -69,7 +69,7 @@ func TestRunCommand(t *testing.T) { conn.Query(ENABLE_PGTAP). ReplyError(pgerrcode.DuplicateObject, `extension "pgtap" already exists, skipping`) // Run test - err := Run(context.Background(), nil, dbConfig, fsys, conn.Intercept) + err := Run(context.Background(), nil, dbConfig, false, fsys, conn.Intercept) // Check error assert.ErrorContains(t, err, "failed to enable pgTAP") }) @@ -93,7 +93,7 @@ func TestRunCommand(t *testing.T) { Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(config.Images.PgProve) + "/json"). ReplyError(errNetwork) // Run test - err := Run(context.Background(), nil, dbConfig, fsys, conn.Intercept) + err := Run(context.Background(), nil, dbConfig, false, fsys, conn.Intercept) // Check error assert.ErrorIs(t, err, errNetwork) assert.Empty(t, apitest.ListUnmatchedRequests()) diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index 44b58cdb0..cf1ccb3a8 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -27,7 +27,7 @@ enabled = false [db] # Port to use for the local database URL. port = 54322 -# Port used by db diff command to initialize the shadow database. +# Port used by db diff and test db commands to initialize the shadow database. shadow_port = 54320 # Maximum amount of time to wait for health check when starting the local database. health_timeout = "2m" From 7513237fefe73067f87d4caaff8f46331ad34493 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Thu, 19 Feb 2026 19:20:29 -0700 Subject: [PATCH 2/3] docs: add --use-shadow-db documentation to test db command Document the --use-shadow-db flag in the CLI man page with usage details, shadow port config, and CI guidance. Add a shadow-db example to examples.yaml and fix the stale shadow_port comment in testdata/config.toml to match the production template. --- docs/supabase/test/db.md | 13 +++++++++++++ docs/templates/examples.yaml | 11 +++++++++++ pkg/config/testdata/config.toml | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/supabase/test/db.md b/docs/supabase/test/db.md index 9978bb453..b09aa6620 100644 --- a/docs/supabase/test/db.md +++ b/docs/supabase/test/db.md @@ -7,3 +7,16 @@ Requires the local development stack to be started by running `supabase start`. Runs `pg_prove` in a container with unit test files volume mounted from `supabase/tests` directory. The test file can be suffixed by either `.sql` or `.pg` extension. Since each test is wrapped in its own transaction, it will be individually rolled back regardless of success or failure. + +## Running tests against a shadow database + +Pass `--use-shadow-db` to run tests against an ephemeral shadow database instead of the local dev database. When this flag is set, the CLI: + +1. Spins up a temporary Postgres container +2. Replays all local migrations from `supabase/migrations` +3. Runs the pgTAP tests against this clean database +4. Destroys the container when finished + +Your local dev database is never touched, making this ideal for CI pipelines and ensuring tests always run against a clean, migration-defined schema. + +The shadow database uses the `shadow_port` configured in `config.toml` (default `54320`) — the same port used by `db diff`. Because they share this port, you cannot run `db diff` and `db test --use-shadow-db` simultaneously. diff --git a/docs/templates/examples.yaml b/docs/templates/examples.yaml index d0588b6fe..844203624 100644 --- a/docs/templates/examples.yaml +++ b/docs/templates/examples.yaml @@ -295,6 +295,17 @@ supabase-test-db: All tests successful. Files=2, Tests=2, 6 wallclock secs ( 0.03 usr 0.01 sys + 0.05 cusr 0.02 csys = 0.11 CPU) Result: PASS + - id: shadow-db + name: Run tests against an isolated shadow database + code: supabase test db --use-shadow-db + response: | + Creating shadow database... + Applying migration 20220810154537_create_employees_table.sql... + /tmp/supabase/tests/nested/order_test.pg .. ok + /tmp/supabase/tests/pet_test.sql .......... ok + All tests successful. + Files=2, Tests=2, 6 wallclock secs ( 0.03 usr 0.01 sys + 0.05 cusr 0.02 csys = 0.11 CPU) + Result: PASS # TODO: use actual cli response for sso commands supabase-sso-show: - id: basic-usage diff --git a/pkg/config/testdata/config.toml b/pkg/config/testdata/config.toml index b228a9c07..69da8804d 100644 --- a/pkg/config/testdata/config.toml +++ b/pkg/config/testdata/config.toml @@ -27,7 +27,7 @@ key_path = "../certs/my-key.pem" [db] # Port to use for the local database URL. port = 54322 -# Port used by db diff command to initialize the shadow database. +# Port used by db diff and test db commands to initialize the shadow database. shadow_port = 54320 # Maximum amount of time to wait for health check when starting the local database. health_timeout = "2m" From a0b0f5cc23820c12cd62b43b5428f3639c39d0f5 Mon Sep 17 00:00:00 2001 From: Geoffrey Sechter Date: Thu, 19 Feb 2026 19:23:16 -0700 Subject: [PATCH 3/3] chore: regenerate API types to include replication_connected field Running go generate picks up the upstream OpenAPI spec change that added ReplicationConnected to V1ServiceHealthResponseInfo1. Fixes the codegen CI check. --- pkg/api/types.gen.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 859b50972..330fc5de8 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -4678,7 +4678,8 @@ type V1ServiceHealthResponseInfo1 struct { // Healthy Deprecated. Use `status` instead. // Deprecated: - Healthy bool `json:"healthy"` + Healthy bool `json:"healthy"` + ReplicationConnected bool `json:"replication_connected"` } // V1ServiceHealthResponseInfo2 defines model for .