Skip to content

Commit 94d23e1

Browse files
authored
fix: diff schemas using pgkit migra (#3994)
* fix: diff schemas using pgkit migra * chore: update exclusion rules for pg schema diff
1 parent 304f2a4 commit 94d23e1

File tree

6 files changed

+172
-24
lines changed

6 files changed

+172
-24
lines changed

cmd/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ var (
100100
differ := diff.DiffSchemaMigra
101101
if usePgSchema {
102102
differ = diff.DiffPgSchema
103-
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as RLS policies, enums, and grants.")
103+
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.")
104104
}
105105
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, differ, afero.NewOsFs())
106106
},

internal/db/diff/diff.go

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,6 @@ func findDropStatements(out string) []string {
8989
return drops
9090
}
9191

92-
func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) {
93-
conn, err := utils.ConnectByConfig(ctx, config, options...)
94-
if err != nil {
95-
return nil, err
96-
}
97-
defer conn.Close(context.Background())
98-
// RLS policies in auth and storage schemas can be included with -s flag
99-
return migration.ListUserSchemas(ctx, conn)
100-
}
101-
10292
func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) {
10393
// Disable background workers in shadow database
10494
config := start.NewContainerConfig("-c", "max_worker_processes=0")
@@ -178,12 +168,11 @@ func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w
178168
}
179169
}
180170
// Load all user defined schemas
181-
if len(schema) == 0 {
182-
if schema, err = loadSchema(ctx, config, options...); err != nil {
183-
return "", err
184-
}
171+
if len(schema) > 0 {
172+
fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ","))
173+
} else {
174+
fmt.Fprintln(w, "Diffing schemas...")
185175
}
186-
fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ","))
187176
source := utils.ToPostgresURL(shadowConfig)
188177
target := utils.ToPostgresURL(config)
189178
return differ(ctx, source, target, schema)

internal/db/diff/diff_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/supabase/cli/internal/testing/helper"
2525
"github.com/supabase/cli/internal/utils"
2626
"github.com/supabase/cli/internal/utils/flags"
27-
"github.com/supabase/cli/pkg/config"
2827
"github.com/supabase/cli/pkg/migration"
2928
"github.com/supabase/cli/pkg/pgtest"
3029
)
@@ -64,7 +63,7 @@ func TestRun(t *testing.T) {
6463
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-storage", ""))
6564
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-shadow-auth")
6665
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-auth", ""))
67-
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.Images.Migra), "test-migra")
66+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra")
6867
diff := "create table test();"
6968
require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff))
7069
// Setup mock postgres
@@ -285,7 +284,7 @@ create schema public`)
285284
gock.New(utils.Docker.DaemonHost()).
286285
Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db").
287286
Reply(http.StatusOK)
288-
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.Images.Migra), "test-migra")
287+
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra")
289288
gock.New(utils.Docker.DaemonHost()).
290289
Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs").
291290
ReplyError(errors.New("network error"))

internal/db/diff/migra.go

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,52 @@ import (
99
"github.com/docker/docker/api/types/container"
1010
"github.com/docker/docker/api/types/network"
1111
"github.com/go-errors/errors"
12+
"github.com/spf13/viper"
1213
"github.com/supabase/cli/internal/utils"
1314
"github.com/supabase/cli/pkg/config"
1415
)
1516

16-
//go:embed templates/migra.sh
17-
var diffSchemaScript string
17+
var (
18+
//go:embed templates/migra.sh
19+
diffSchemaScript string
20+
//go:embed templates/migra.ts
21+
diffSchemaTypeScript string
22+
23+
managedSchemas = []string{
24+
// Local development
25+
"_analytics",
26+
"_realtime",
27+
"_supavisor",
28+
// Owned by extensions
29+
"cron",
30+
"graphql",
31+
"graphql_public",
32+
"net",
33+
"pgroonga",
34+
"pgtle",
35+
"repack",
36+
"tiger_data",
37+
"vault",
38+
// Deprecated extensions
39+
"pgsodium",
40+
"pgsodium_masks",
41+
"timescaledb_experimental",
42+
"timescaledb_information",
43+
"_timescaledb_cache",
44+
"_timescaledb_catalog",
45+
"_timescaledb_config",
46+
"_timescaledb_debug",
47+
"_timescaledb_functions",
48+
"_timescaledb_internal",
49+
// Managed by Supabase
50+
"pgbouncer",
51+
"supabase_functions",
52+
"supabase_migrations",
53+
}
54+
)
1855

1956
// Diffs local database schema against shadow, dumps output to stdout.
20-
func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) {
57+
func DiffSchemaMigraBash(ctx context.Context, source, target string, schema []string) (string, error) {
2158
env := []string{"SOURCE=" + source, "TARGET=" + target}
2259
// Passing in script string means command line args must be set manually, ie. "$@"
2360
args := "set -- " + strings.Join(schema, " ") + ";"
@@ -42,3 +79,41 @@ func DiffSchemaMigra(ctx context.Context, source, target string, schema []string
4279
}
4380
return out.String(), nil
4481
}
82+
83+
func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) {
84+
env := []string{"SOURCE=" + source, "TARGET=" + target}
85+
if len(schema) > 0 {
86+
env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, ","))
87+
} else {
88+
env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(managedSchemas, ","))
89+
}
90+
cmd := []string{"edge-runtime", "start", "--main-service=."}
91+
if viper.GetBool("DEBUG") {
92+
cmd = append(cmd, "--verbose")
93+
}
94+
cmdString := strings.Join(cmd, " ")
95+
entrypoint := []string{"sh", "-c", `cat <<'EOF' > index.ts && ` + cmdString + `
96+
` + diffSchemaTypeScript + `
97+
EOF
98+
`}
99+
var out, stderr bytes.Buffer
100+
if err := utils.DockerRunOnceWithConfig(
101+
ctx,
102+
container.Config{
103+
Image: utils.Config.EdgeRuntime.Image,
104+
Env: env,
105+
Entrypoint: entrypoint,
106+
},
107+
container.HostConfig{
108+
Binds: []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"},
109+
NetworkMode: network.NetworkHost,
110+
},
111+
network.NetworkingConfig{},
112+
"",
113+
&out,
114+
&stderr,
115+
); err != nil && !strings.HasPrefix(stderr.String(), "main worker has been destroyed") {
116+
return "", errors.Errorf("error diffing schema: %w:\n%s", err, stderr.String())
117+
}
118+
return out.String(), nil
119+
}

internal/db/diff/pgschema.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,24 @@ func DiffPgSchema(ctx context.Context, source, target string, schema []string) (
2222
}
2323
defer dbDst.Close()
2424
// Generate DDL based on schema plan
25+
opts := []pgschema.PlanOpt{pgschema.WithDoNotValidatePlan()}
26+
if len(schema) > 0 {
27+
opts = append(opts, pgschema.WithIncludeSchemas(schema...))
28+
} else {
29+
opts = append(opts,
30+
pgschema.WithExcludeSchemas(managedSchemas...),
31+
pgschema.WithExcludeSchemas(
32+
"topology", // unsupported due to views
33+
"realtime", // unsupported due to partitioned table
34+
"storage", // unsupported due to unique index
35+
),
36+
)
37+
}
2538
plan, err := pgschema.Generate(
2639
ctx,
2740
pgschema.DBSchemaSource(dbSrc),
2841
pgschema.DBSchemaSource(dbDst),
29-
pgschema.WithDoNotValidatePlan(),
30-
pgschema.WithIncludeSchemas(schema...),
42+
opts...,
3143
)
3244
if err != nil {
3345
return "", errors.Errorf("failed to generate plan: %w", err)

internal/db/diff/templates/migra.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createClient } from "npm:@pgkit/client";
2+
import { Migration } from "npm:@pgkit/migra";
3+
4+
const clientBase = createClient(Deno.env.get("SOURCE"));
5+
const clientHead = createClient(Deno.env.get("TARGET"));
6+
const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS")?.split(",") ?? [];
7+
const excludedSchemas = Deno.env.get("EXCLUDED_SCHEMAS")?.split(",") ?? [];
8+
9+
const managedSchemas = ["auth", "realtime", "storage"];
10+
const extensionSchemas = [
11+
"pg_catalog",
12+
"extensions",
13+
"pgmq",
14+
"tiger",
15+
"topology",
16+
];
17+
18+
try {
19+
let sql = "";
20+
for (const schema of includedSchemas) {
21+
const m = await Migration.create(clientBase, clientHead, {
22+
schema,
23+
ignore_extension_versions: true,
24+
});
25+
m.set_safety(false);
26+
m.add_all_changes(true);
27+
sql += m.sql;
28+
}
29+
if (includedSchemas.length === 0) {
30+
// Migra does not ignore custom types and triggers created by extensions, so we diff
31+
// them separately. This workaround only applies to a known list of managed schemas.
32+
for (const schema of extensionSchemas) {
33+
const e = await Migration.create(clientBase, clientHead, {
34+
schema,
35+
ignore_extension_versions: true,
36+
});
37+
e.set_safety(false);
38+
e.add(e.changes.schemas({ creations_only: true }));
39+
e.add_extension_changes();
40+
sql += e.sql;
41+
}
42+
// Diff user defined entities in non-managed schemas, including extensions.
43+
const m = await Migration.create(clientBase, clientHead, {
44+
exclude_schema: [
45+
...managedSchemas,
46+
...extensionSchemas,
47+
...excludedSchemas,
48+
],
49+
ignore_extension_versions: true,
50+
});
51+
m.set_safety(false);
52+
m.add_all_changes(true);
53+
sql += m.sql;
54+
// For managed schemas, we want to include triggers and RLS policies only.
55+
for (const schema of managedSchemas) {
56+
const s = await Migration.create(clientBase, clientHead, {
57+
schema,
58+
ignore_extension_versions: true,
59+
});
60+
s.set_safety(false);
61+
s.add(s.changes.triggers({ drops_only: true }));
62+
s.add(s.changes.rlspolicies({ drops_only: true }));
63+
s.add(s.changes.rlspolicies({ creations_only: true }));
64+
s.add(s.changes.triggers({ creations_only: true }));
65+
sql += s.sql;
66+
}
67+
}
68+
console.log(sql);
69+
} catch (e) {
70+
console.error(e);
71+
} finally {
72+
await Promise.all([clientHead.end(), clientBase.end()]);
73+
}

0 commit comments

Comments
 (0)