Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/cli-go/cmd/db_schema_declarative.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/supabase/cli/internal/db/reset"
"github.com/supabase/cli/internal/db/start"
"github.com/supabase/cli/internal/migration/new"
"github.com/supabase/cli/internal/pgdelta"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/config"
Expand Down Expand Up @@ -107,6 +108,14 @@ var (
RunE: runDeclarativeSync,
}

// dbDeclarativeApplyCmd applies declarative files directly to the local database
// without creating a timestamped migration.
dbDeclarativeApplyCmd = &cobra.Command{
Use: "apply",
Short: "Apply declarative schema to the local database",
RunE: runDeclarativeApply,
}

// dbDeclarativeGenerateCmd generates declarative files directly from a live
// database target. This is the entrypoint for bootstrapping declarative mode.
dbDeclarativeGenerateCmd = &cobra.Command{
Expand Down Expand Up @@ -236,6 +245,22 @@ func configureLocalDbConfig() {
}
}

func runDeclarativeApply(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
fsys := afero.NewOsFs()

if !hasDeclarativeFiles(fsys) {
return fmt.Errorf("no declarative schema found. Run %s first", utils.Aqua("supabase db schema declarative generate"))
}
if err := ensureLocalDatabaseStarted(ctx, true, utils.AssertSupabaseDbIsRunning, func(ctx context.Context) error {
return start.Run(ctx, "", fsys)
}); err != nil {
return err
}
configureLocalDbConfig()
return pgdelta.ApplyDeclarative(ctx, flags.DbConfig, fsys)
Comment thread
ametel01 marked this conversation as resolved.
}

// runDeclarativeGenerate implements the smart interactive generate flow.
func runDeclarativeGenerate(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
Expand Down Expand Up @@ -571,6 +596,7 @@ func init() {
generateFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", generateFlags.Lookup("password")))

dbDeclarativeCmd.AddCommand(dbDeclarativeApplyCmd)
dbDeclarativeCmd.AddCommand(dbDeclarativeSyncCmd)
dbDeclarativeCmd.AddCommand(dbDeclarativeGenerateCmd)
dbSchemaCmd.AddCommand(dbDeclarativeCmd)
Expand Down
8 changes: 8 additions & 0 deletions apps/cli-go/cmd/db_schema_declarative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ func TestHasMigrationFiles(t *testing.T) {
})
}

func TestDeclarativeApplyCommandRegistered(t *testing.T) {
cmd, _, err := dbDeclarativeCmd.Find([]string{"apply"})

require.NoError(t, err)
require.NotNil(t, cmd)
assert.Equal(t, "apply", cmd.Name())
}

func TestSaveApplyDebugBundle(t *testing.T) {
t.Run("saves debug artifacts with expected content", func(t *testing.T) {
fsys := afero.NewMemMapFs()
Expand Down
7 changes: 7 additions & 0 deletions apps/cli-go/docs/supabase/db/schema-declarative-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## supabase-db-schema-declarative-apply

Apply declarative schema files directly to the local database.

Reads SQL files from the configured declarative schema directory and applies them to the local database using pg-delta without creating a timestamped migration. This is intended for local or CI bootstrapping, not as a replacement for migrations in controlled schema evolution.

Requires `--experimental` flag or `[experimental.pgdelta] enabled = true` in config.
8 changes: 6 additions & 2 deletions apps/cli-go/internal/pgdelta/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,9 @@ func ApplyDeclarative(ctx context.Context, config pgconn.Config, fsys afero.Fs)
fmt.Fprintln(os.Stderr, "Applying declarative schemas via pg-delta...")
var stdout, stderr bytes.Buffer
script := pkgconfig.InterpolatePgDeltaScript(pkgconfig.Config(&utils.Config), pgDeltaDeclarativeApplyScript)
if err := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error running pg-delta script", &stdout, &stderr, utils.PgDeltaNpmRegistryOption()); err != nil {
return err
runErr := utils.RunEdgeRuntimeScript(ctx, env, script, binds, "error running pg-delta script", &stdout, &stderr, utils.PgDeltaNpmRegistryOption())
if runErr != nil && len(bytes.TrimSpace(stdout.Bytes())) == 0 {
return runErr
}

var result ApplyResult
Expand All @@ -349,6 +350,9 @@ func ApplyDeclarative(ctx context.Context, config pgconn.Config, fsys afero.Fs)
}
return errors.Errorf("pg-delta declarative apply failed with status: %s", result.Status)
}
if runErr != nil {
return runErr
}
fmt.Fprintf(os.Stderr, "Applied %d statements in %d round(s).\n", result.TotalApplied, result.TotalRounds)
return nil
}
6 changes: 6 additions & 0 deletions apps/cli-go/internal/pgdelta/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ func TestFormatApplyFailure(t *testing.T) {
assertContains(t, formatted, `SQL: CREATE EXTENSION pgmq WITH SCHEMA pgmq;`)
}

func TestDeclarativeApplyTemplateDoesNotThrowForStructuredNonSuccess(t *testing.T) {
assertContains(t, pgDeltaDeclarativeApplyScript, "console.log(JSON.stringify(payload));")
assertNotContains(t, pgDeltaDeclarativeApplyScript, `apply.status !== "success"`)
assertNotContains(t, pgDeltaDeclarativeApplyScript, "pg-delta apply failed with status")
}

// TestApplyResultUnmarshalValidationErrors reproduces the payload shape pg-delta
// emits when the final check_function_bodies=on pass fails: totalApplied
// matches totalStatements, errors and stuckStatements are empty, but status is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ try {
diagnostics: result.diagnostics ?? [],
};
console.log(JSON.stringify(payload));
if (apply.status !== "success") {
throw new Error("pg-delta apply failed with status: " + apply.status);
}
}
} catch (e) {
throw e instanceof Error ? e : new Error(String(e));
Expand Down
1 change: 1 addition & 0 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ Legend:
| `db remote commit` | `wrapped` | [`../src/legacy/commands/db/remote/commit/commit.command.ts`](../src/legacy/commands/db/remote/commit/commit.command.ts) |
| `db schema declarative sync` | `ported` | [`../src/legacy/commands/db/schema/declarative/sync/sync.command.ts`](../src/legacy/commands/db/schema/declarative/sync/sync.command.ts) |
| `db schema declarative generate` | `ported` | [`../src/legacy/commands/db/schema/declarative/generate/generate.command.ts`](../src/legacy/commands/db/schema/declarative/generate/generate.command.ts) |
| `db schema declarative apply` | `ported` | [`../src/legacy/commands/db/schema/declarative/apply/apply.command.ts`](../src/legacy/commands/db/schema/declarative/apply/apply.command.ts) — native pg-delta direct local apply |

Flag divergences from the Go reference:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# `supabase db schema declarative apply`

Applies existing declarative schema files directly to the local database using
pg-delta. It does not create a timestamped migration file and does not update
local migration history.

## Files Read

| Path | Format | When |
| --------------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------------------------------------- |
| `<workdir>/supabase/config.toml` | TOML | always — pg-delta gate, local DB port/password |
| `<workdir>/supabase/.temp/pgdelta-version` | plain text | always — pins the `@supabase/pg-delta` npm version |
| `<workdir>/supabase/.temp/edge-runtime-version` | plain text | always — pins the edge-runtime image tag |
| `<workdir>/supabase/database/**/*.sql` (declarative dir; configurable via `[experimental.pgdelta] declarative_schema_path`) | SQL | always — must exist and is mounted read-only |

## Files Written

| Path | Format | When |
| ---------------------------- | ------ | ----------------------------------------------- |
| `~/.supabase/telemetry.json` | JSON | always (in `Effect.ensuring`) at end of command |

This command does **not** write `supabase/migrations/*.sql` and does **not**
update migration history.

## Subprocesses / Containers

| Process | Condition |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `supabase-go db start` via the declarative seam | when the local database container is not running |
| Edge-runtime container (`supabase/edge-runtime`) running the pg-delta declarative-apply Deno script | always after validation |

## API Routes

None.

## Environment Variables

| Variable | Purpose | Required? |
| ---------------------------- | ------------------------------------------------ | --------- |
| `PGDELTA_NPM_REGISTRY` | private `@supabase` npm registry for pg-delta | no |
| `PGDELTA_DEBUG` | verbose pg-delta diagnostics | no |
| `SUPABASE_GO_BINARY` | override the `supabase-go` seam binary | no |
| `SUPABASE_SERVICES_HOSTNAME` | local DB host (Go `GetHostname`) | no |
| `DOCKER_HOST` | tcp daemon host used as the local DB host backup | no |

## Exit Codes

| Exit | Meaning |
| ---- | ------------------------------------------------------------------------------------------------- |
| `0` | declarative schema applied successfully |
| `1` | pg-delta disabled; no declarative files found; local database start failed; pg-delta apply failed |

## Telemetry Events Fired

| Event | When | Notable properties / groups |
| ---------------------- | ------------------------------------------ | ----------------------------------- |
| `cli_command_executed` | post-run, success or failure (via wrapper) | `exit_code`, `duration_ms`, `flags` |

## Output

Text mode only; the command has no command-specific machine envelope. Global
`--output-format json` / `stream-json` error handling still emits the standard
wrapper error format on failures, but successful progress output remains stderr
text.

### `--output-format text` (Go CLI compatible)

Progress output is written to stderr:

- `Applying declarative schemas via pg-delta...`
- `Applied <n> statements in <r> round(s).`

Apply failures include pg-delta's structured status summary before returning an
error.

### `--output-format json` / `stream-json`

No success payload is emitted. Successful output remains the stderr text above.
On failure, the shared output wrapper emits its normal JSON / stream-json error.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Effect } from "effect";
import { Command } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";

import { withJsonErrorHandling } from "../../../../../../shared/output/json-error-handling.ts";
import { withLegacyCommandInstrumentation } from "../../../../../telemetry/legacy-command-instrumentation.ts";
import { legacyDbSchemaDeclarativeSharedBase } from "../declarative.shared.ts";
import { legacyDbSchemaDeclarativeApply } from "./apply.handler.ts";
import { legacyDbSchemaDeclarativeApplyRuntimeLayer } from "./apply.layers.ts";

const config = {} as const;

export type LegacyDbSchemaDeclarativeApplyFlags = CliCommand.Command.Config.Infer<typeof config> & {
readonly noCache: boolean;
};

export const legacyDbSchemaDeclarativeApplyCommand = Command.make("apply", config).pipe(
Command.withDescription("Apply declarative schema to the local database."),
Command.withShortDescription("Apply declarative schema to the local database"),
Command.withHandler((flags) =>
Effect.gen(function* () {
const shared = yield* legacyDbSchemaDeclarativeSharedBase;
const merged: LegacyDbSchemaDeclarativeApplyFlags = { ...flags, noCache: shared.noCache };
return yield* legacyDbSchemaDeclarativeApply(merged).pipe(
withLegacyCommandInstrumentation({
flags: { "no-cache": merged.noCache },
}),
withJsonErrorHandling,
);
}),
),
Command.provide(legacyDbSchemaDeclarativeApplyRuntimeLayer),
);
Loading