Repository: supabase/cli
Pull request #2508 (2024-07-10 10:12:00)

feat: expose batch upsert api for storage buckets
## What kind of change does this PR introduce?

feature

## What is the new behavior?

Exposes additional methods
- `storage.UpsertBuckets`
- `migration.DropUserSchemas`
- `migration.ListUserSchemas`

Pass in project ref so that the local project is seeded on supabase start.

## Additional context

Add any other context or screenshots.


coveralls: ## Pull Request Test Coverage Report for [Build 9872307584](https://coveralls.io/builds/68562467)


### Details

* **19** of **22**   **(86.36%)**  changed or added relevant lines in **5** files are covered.
* **9** unchanged lines in **4** files lost coverage.
* Overall coverage increased (+**0.2%**) to **60.111%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/seed.go](https://coveralls.io/builds/68562467/source?filename=cmd%2Fseed.go#L30) | 0 | 1 | 0.0%
| [internal/seed/buckets/buckets.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fseed%2Fbuckets%2Fbuckets.go#L24) | 11 | 12 | 91.67%
| [internal/storage/cp/cp.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fstorage%2Fcp%2Fcp.go#L143) | 3 | 4 | 75.0%
<!-- | **Total:** | **19** | **22** | **86.36%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/start/start.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fstart%2Fstart.go#L1053) | 1 | 70.06% |
| [internal/storage/cp/cp.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fstorage%2Fcp%2Fcp.go#L144) | 1 | 91.87% |
| [internal/storage/rm/rm.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fstorage%2Frm%2Frm.go#L124) | 2 | 89.53% |
| [internal/gen/keys/keys.go](https://coveralls.io/builds/68562467/source?filename=internal%2Fgen%2Fkeys%2Fkeys.go#L87) | 5 | 12.9% |
<!-- | **Total:** | **9** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/68562467/badge)](https://coveralls.io/builds/68562467) |
| :-- | --: |
| Change from base [Build 9868558600](https://coveralls.io/builds/68557223): |  0.2% |
| Covered Lines: | 6290 |
| Relevant Lines: | 10464 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/cmd/seed.go b/cmd/seed.go
index c191b48d..760a9424 100644
--- a/cmd/seed.go
+++ b/cmd/seed.go
@@ -7,6 +7,7 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/supabase/cli/internal/seed/buckets"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/internal/utils/flags"
 )
 
 var (
@@ -26,7 +27,7 @@ var (
 		Short: "Seed buckets declared in [storage.buckets]",
 		Args:  cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return buckets.Run(cmd.Context(), utils.NewConsole())
+			return buckets.Run(cmd.Context(), flags.ProjectRef, utils.NewConsole())
 		},
 	}
 )
diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go
index a36180f9..88b420d8 100644
--- a/internal/db/diff/diff.go
+++ b/internal/db/diff/diff.go
@@ -21,7 +21,6 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/db/start"
 	"github.com/supabase/cli/internal/gen/keys"
 	"github.com/supabase/cli/internal/migration/apply"
@@ -128,7 +127,7 @@ func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.
 	}
 	defer conn.Close(context.Background())
 	// RLS policies in auth and storage schemas can be included with -s flag
-	return reset.LoadUserSchemas(ctx, conn)
+	return migration.ListUserSchemas(ctx, conn)
 }
 
 func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) {
diff --git a/internal/db/lint/lint.go b/internal/db/lint/lint.go
index b65b44e4..dc302600 100644
--- a/internal/db/lint/lint.go
+++ b/internal/db/lint/lint.go
@@ -13,8 +13,8 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/migration"
 )
 
 const ENABLE_PGSQL_CHECK = "CREATE EXTENSION IF NOT EXISTS plpgsql_check"
@@ -93,7 +93,7 @@ func LintDatabase(ctx context.Context, conn *pgx.Conn, schema []string) ([]Resul
 		return nil, errors.Errorf("failed to begin transaction: %w", err)
 	}
 	if len(schema) == 0 {
-		schema, err = reset.LoadUserSchemas(ctx, conn)
+		schema, err = migration.ListUserSchemas(ctx, conn)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/db/pull/pull.go b/internal/db/pull/pull.go
index c82761c5..f6a235e1 100644
--- a/internal/db/pull/pull.go
+++ b/internal/db/pull/pull.go
@@ -14,7 +14,6 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/db/diff"
 	"github.com/supabase/cli/internal/db/dump"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/migration/new"
 	"github.com/supabase/cli/internal/migration/repair"
@@ -79,7 +78,7 @@ func run(p utils.Program, ctx context.Context, schema []string, path string, con
 	defaultSchema := len(schema) == 0
 	if defaultSchema {
 		var err error
-		schema, err = reset.LoadUserSchemas(ctx, conn)
+		schema, err = migration.ListUserSchemas(ctx, conn)
 		if err != nil {
 			return err
 		}
diff --git a/internal/db/remote/changes/changes.go b/internal/db/remote/changes/changes.go
index d35defd1..c735b598 100644
--- a/internal/db/remote/changes/changes.go
+++ b/internal/db/remote/changes/changes.go
@@ -7,8 +7,8 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/db/diff"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/migration"
 )
 
 var output string
@@ -49,5 +49,5 @@ func loadSchema(ctx context.Context, config pgconn.Config, w io.Writer) ([]strin
 		return nil, err
 	}
 	defer conn.Close(context.Background())
-	return reset.LoadUserSchemas(ctx, conn)
+	return migration.ListUserSchemas(ctx, conn)
 }
diff --git a/internal/db/remote/commit/commit.go b/internal/db/remote/commit/commit.go
index 1f793f4e..fdab9691 100644
--- a/internal/db/remote/commit/commit.go
+++ b/internal/db/remote/commit/commit.go
@@ -12,7 +12,6 @@ import (
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/db/diff"
 	"github.com/supabase/cli/internal/db/dump"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/migration"
@@ -48,7 +47,7 @@ func run(p utils.Program, ctx context.Context, schema []string, config pgconn.Co
 
 	// 2. Fetch remote schema changes
 	if len(schema) == 0 {
-		schema, err = reset.LoadUserSchemas(ctx, conn)
+		schema, err = migration.ListUserSchemas(ctx, conn)
 		if err != nil {
 			return err
 		}
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index 08fe208f..cb856899 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -23,14 +23,6 @@ import (
 	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/pkg/migration"
-	"github.com/supabase/cli/pkg/pgxv5"
-)
-
-var (
-	//go:embed templates/drop.sql
-	dropObjects string
-	//go:embed templates/list.sql
-	ListSchemas string
 )
 
 func Run(ctx context.Context, version string, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
@@ -225,44 +217,12 @@ func resetRemote(ctx context.Context, version string, config pgconn.Config, fsys
 		return err
 	}
 	defer conn.Close(context.Background())
-	// Only drop objects in extensions and public schema
-	excludes := append([]string{
-		"extensions",
-		"public",
-	}, utils.ManagedSchemas...)
-	userSchemas, err := LoadUserSchemas(ctx, conn, excludes...)
-	if err != nil {
-		return err
-	}
-	// Drop all user defined schemas
-	migration := migration.MigrationFile{}
-	for _, schema := range userSchemas {
-		sql := fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schema)
-		migration.Statements = append(migration.Statements, sql)
-	}
-	// If an extension uses a schema it doesn't create, dropping the schema will cascade to also
-	// drop the extension. But if an extension creates its own schema, dropping the schema will
-	// throw an error. Hence, we drop the extension instead so it cascades to its own schema.
-	migration.Statements = append(migration.Statements, dropObjects)
-	if err := migration.ExecBatch(ctx, conn); err != nil {
+	if err := migration.DropUserSchemas(ctx, conn); err != nil {
 		return err
 	}
 	return apply.MigrateAndSeed(ctx, version, conn, fsys)
 }
 
-func LoadUserSchemas(ctx context.Context, conn *pgx.Conn, exclude ...string) ([]string, error) {
-	if len(exclude) == 0 {
-		exclude = utils.ManagedSchemas
-	}
-	exclude = LikeEscapeSchema(exclude)
-	rows, err := conn.Query(ctx, ListSchemas, exclude)
-	if err != nil {
-		return nil, errors.Errorf("failed to list schemas: %w", err)
-	}
-	// TODO: show detail and hint from pgconn.PgError
-	return pgxv5.CollectStrings(rows)
-}
-
 func LikeEscapeSchema(schemas []string) (result []string) {
 	// Treat _ as literal, * as any character
 	replacer := strings.NewReplacer("_", `\_`, "*", "%")
diff --git a/internal/seed/buckets/buckets.go b/internal/seed/buckets/buckets.go
index ddec994a..cf860f13 100644
--- a/internal/seed/buckets/buckets.go
+++ b/internal/seed/buckets/buckets.go
@@ -3,77 +3,27 @@ package buckets
 import (
 	"context"
 	"fmt"
-	"os"
 
 	"github.com/supabase/cli/internal/storage/client"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/flags"
-	"github.com/supabase/cli/pkg/storage"
 )
 
-func Run(ctx context.Context, console *utils.Console) error {
-	api, err := client.NewStorageAPI(ctx, flags.ProjectRef)
+func Run(ctx context.Context, projectRef string, console *utils.Console) error {
+	api, err := client.NewStorageAPI(ctx, projectRef)
 	if err != nil {
 		return err
 	}
-	buckets, err := api.ListBuckets(ctx)
-	if err != nil {
-		return err
-	}
-	var exists []string
-	for _, b := range buckets {
-		props := NewBucketProps(b.Name)
-		if props == nil {
-			continue
-		}
-		exists = append(exists, b.Name)
-		label := fmt.Sprintf("Bucket %s already exists. Do you want to overwrite its properties?", utils.Bold(b.Id))
-		if shouldOverwrite, err := console.PromptYesNo(ctx, label, true); err != nil {
-			return err
-		} else if !shouldOverwrite {
-			continue
-		}
-		body := storage.UpdateBucketRequest{
-			Id:          b.Id,
-			BucketProps: props,
-		}
-		if _, err := api.UpdateBucket(ctx, body); err != nil {
-			return err
-		}
+	config := utils.Config.Storage.GetBucketConfig()
+	if console == nil {
+		return api.UpsertBuckets(ctx, config)
 	}
-	for name := range utils.Config.Storage.Buckets {
-		if utils.SliceContains(exists, name) {
-			continue
-		}
-		msg := "Creating storage bucket:"
-		if len(flags.ProjectRef) == 0 {
-			msg = "Creating local storage bucket:"
+	filter := func(bucketId string) bool {
+		label := fmt.Sprintf("Bucket %s already exists. Do you want to overwrite its properties?", utils.Bold(bucketId))
+		shouldOverwrite, err := console.PromptYesNo(ctx, label, true)
+		if err != nil {
+			fmt.Fprintln(utils.GetDebugLogger(), err)
 		}
-		fmt.Fprintln(os.Stderr, msg, name)
-		body := storage.CreateBucketRequest{
-			Name:        name,
-			BucketProps: NewBucketProps(name),
-		}
-		if _, err := api.CreateBucket(ctx, body); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func NewBucketProps(name string) *storage.BucketProps {
-	config, ok := utils.Config.Storage.Buckets[name]
-	if !ok {
-		return nil
-	}
-	props := storage.BucketProps{
-		Public:           config.Public,
-		AllowedMimeTypes: config.AllowedMimeTypes,
-	}
-	if config.FileSizeLimit > 0 {
-		props.FileSizeLimit = int(config.FileSizeLimit)
-	} else {
-		props.FileSizeLimit = int(utils.Config.Storage.FileSizeLimit)
+		return shouldOverwrite
 	}
-	return &props
+	return api.UpsertBuckets(ctx, config, filter)
 }
diff --git a/internal/start/start.go b/internal/start/start.go
index c3e5c092..07e9293b 100644
--- a/internal/start/start.go
+++ b/internal/start/start.go
@@ -1049,9 +1049,7 @@ EOF
 			return err
 		}
 		// Disable prompts when seeding
-		console := utils.NewConsole()
-		console.IsTTY = false
-		if err := buckets.Run(ctx, console); err != nil {
+		if err := buckets.Run(ctx, "", nil); err != nil {
 			return err
 		}
 	}
diff --git a/internal/storage/cp/cp.go b/internal/storage/cp/cp.go
index 8dd03933..fa5dd506 100644
--- a/internal/storage/cp/cp.go
+++ b/internal/storage/cp/cp.go
@@ -12,7 +12,6 @@ import (
 
 	"github.com/go-errors/errors"
 	"github.com/spf13/afero"
-	"github.com/supabase/cli/internal/seed/buckets"
 	"github.com/supabase/cli/internal/storage/client"
 	"github.com/supabase/cli/internal/storage/ls"
 	"github.com/supabase/cli/internal/utils"
@@ -106,6 +105,7 @@ func UploadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remoteP
 	}); err != nil {
 		return err
 	}
+	config := utils.Config.Storage.GetBucketConfig()
 	baseName := filepath.Base(localPath)
 	jq := utils.NewJobQueue(maxJobs)
 	err := afero.Walk(fsys, localPath, func(filePath string, info fs.FileInfo, err error) error {
@@ -138,9 +138,9 @@ func UploadStorageObjectAll(ctx context.Context, api storage.StorageAPI, remoteP
 			if err != nil && strings.Contains(err.Error(), `"error":"Bucket not found"`) {
 				// Retry after creating bucket
 				if bucket, prefix := client.SplitBucketPrefix(dstPath); len(prefix) > 0 {
-					body := storage.CreateBucketRequest{
-						Name:        bucket,
-						BucketProps: buckets.NewBucketProps(bucket),
+					body := storage.CreateBucketRequest{Name: bucket}
+					if props, ok := config[bucket]; ok {
+						body.BucketProps = &props
 					}
 					if _, err := api.CreateBucket(ctx, body); err != nil {
 						return err
diff --git a/internal/utils/console.go b/internal/utils/console.go
index f94ce10d..dfd014af 100644
--- a/internal/utils/console.go
+++ b/internal/utils/console.go
@@ -4,7 +4,6 @@ import (
 	"bufio"
 	"context"
 	"fmt"
-	"io"
 	"os"
 	"strings"
 	"sync"
@@ -15,20 +14,18 @@ import (
 )
 
 type Console struct {
-	IsTTY  bool
-	stdin  *bufio.Scanner
-	logger io.Writer
-	token  chan string
-	mu     sync.Mutex
+	IsTTY bool
+	stdin *bufio.Scanner
+	token chan string
+	mu    sync.Mutex
 }
 
 func NewConsole() *Console {
 	return &Console{
-		IsTTY:  term.IsTerminal(int(os.Stdin.Fd())),
-		stdin:  bufio.NewScanner(os.Stdin),
-		logger: GetDebugLogger(),
-		token:  make(chan string),
-		mu:     sync.Mutex{},
+		IsTTY: term.IsTerminal(int(os.Stdin.Fd())),
+		stdin: bufio.NewScanner(os.Stdin),
+		token: make(chan string),
+		mu:    sync.Mutex{},
 	}
 }
 
diff --git a/internal/utils/misc.go b/internal/utils/misc.go
index db6854b9..5971de86 100644
--- a/internal/utils/misc.go
+++ b/internal/utils/misc.go
@@ -68,16 +68,6 @@ var (
 		"pg_*", // Wildcard pattern follows pg_dump
 	}
 	// Initialised by postgres image and owned by postgres role
-	ManagedSchemas = append([]string{
-		"_analytics",
-		"_realtime",
-		"_supavisor",
-		"pgbouncer",
-		"pgsodium",
-		"pgtle",
-		"supabase_migrations",
-		"vault",
-	}, PgSchemas...)
 	InternalSchemas = append([]string{
 		"_analytics",
 		"_realtime",
diff --git a/pkg/config/utils.go b/pkg/config/utils.go
index 755f4867..ca9af42c 100644
--- a/pkg/config/utils.go
+++ b/pkg/config/utils.go
@@ -3,6 +3,8 @@ package config
 import (
 	"path/filepath"
 	"strings"
+
+	types "github.com/supabase/cli/pkg/storage"
 )
 
 type pathBuilder struct {
@@ -68,6 +70,23 @@ func NewPathBuilder(configPath string) pathBuilder {
 	}
 }
 
+func (s *storage) GetBucketConfig() map[string]types.BucketProps {
+	result := make(map[string]types.BucketProps, len(s.Buckets))
+	for name, bucket := range s.Buckets {
+		props := types.BucketProps{
+			Public:           bucket.Public,
+			AllowedMimeTypes: bucket.AllowedMimeTypes,
+		}
+		if bucket.FileSizeLimit > 0 {
+			props.FileSizeLimit = int(bucket.FileSizeLimit)
+		} else {
+			props.FileSizeLimit = int(s.FileSizeLimit)
+		}
+		result[name] = props
+	}
+	return result
+}
+
 func ptr[T any](v T) *T {
 	return &v
 }
@@ -81,8 +100,8 @@ func sliceContains[T comparable](s []T, e T) bool {
 	return false
 }
 
-func removeDuplicates(slice []string) (result []string) {
-	set := make(map[string]struct{})
+func removeDuplicates[T comparable](slice []T) (result []T) {
+	set := make(map[T]struct{})
 	for _, item := range slice {
 		if _, exists := set[item]; !exists {
 			set[item] = struct{}{}
diff --git a/pkg/migration/apply.go b/pkg/migration/apply.go
index 0311b43f..fe8e4123 100644
--- a/pkg/migration/apply.go
+++ b/pkg/migration/apply.go
@@ -43,12 +43,10 @@ func FindPendingMigrations(localMigrations, remoteMigrations []string) ([]string
 		missing = append(missing, remoteMigrations[i:]...)
 	}
 	if len(missing) > 0 {
-		// utils.CmdSuggestion = suggestRevertHistory(missing)
 		return missing, errors.New(ErrMissingLocal)
 	}
 	// Enforce migrations are applied in chronological order by default
 	if len(unapplied) > 0 {
-		// utils.CmdSuggestion = suggestIgnoreFlag(unapplied)
 		return unapplied, errors.New(ErrMissingRemote)
 	}
 	pending := localMigrations[len(remoteMigrations):]
diff --git a/pkg/migration/drop.go b/pkg/migration/drop.go
new file mode 100644
index 00000000..362f2a9d
--- /dev/null
+++ b/pkg/migration/drop.go
@@ -0,0 +1,67 @@
+package migration
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+
+	"github.com/go-errors/errors"
+	"github.com/jackc/pgx/v4"
+	"github.com/supabase/cli/pkg/pgxv5"
+)
+
+var (
+	//go:embed queries/drop.sql
+	DropObjects string
+	//go:embed queries/list.sql
+	ListSchemas string
+
+	// Initialised by postgres image and owned by postgres role
+	ManagedSchemas = []string{
+		`information\_schema`,
+		`pg\_%`,
+		`\_analytics`,
+		`\_realtime`,
+		`\_supavisor`,
+		"pgbouncer",
+		"pgsodium",
+		"pgtle",
+		`supabase\_migrations`,
+		"vault",
+	}
+)
+
+func DropUserSchemas(ctx context.Context, conn *pgx.Conn) error {
+	// Only drop objects in extensions and public schema
+	excludes := append(ManagedSchemas,
+		"extensions",
+		"public",
+	)
+	userSchemas, err := ListUserSchemas(ctx, conn, excludes...)
+	if err != nil {
+		return err
+	}
+	// Drop all user defined schemas
+	migration := MigrationFile{}
+	for _, schema := range userSchemas {
+		sql := fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schema)
+		migration.Statements = append(migration.Statements, sql)
+	}
+	// If an extension uses a schema it doesn't create, dropping the schema will cascade to also
+	// drop the extension. But if an extension creates its own schema, dropping the schema will
+	// throw an error. Hence, we drop the extension instead so it cascades to its own schema.
+	migration.Statements = append(migration.Statements, DropObjects)
+	return migration.ExecBatch(ctx, conn)
+}
+
+func ListUserSchemas(ctx context.Context, conn *pgx.Conn, exclude ...string) ([]string, error) {
+	if len(exclude) == 0 {
+		exclude = ManagedSchemas
+	}
+	rows, err := conn.Query(ctx, ListSchemas, exclude)
+	if err != nil {
+		return nil, errors.Errorf("failed to list schemas: %w", err)
+	}
+	// TODO: show detail and hint from pgconn.PgError
+	return pgxv5.CollectStrings(rows)
+}
diff --git a/internal/db/reset/templates/drop.sql b/pkg/migration/queries/drop.sql
similarity index 100%
rename from internal/db/reset/templates/drop.sql
rename to pkg/migration/queries/drop.sql
diff --git a/internal/db/reset/templates/list.sql b/pkg/migration/queries/list.sql
similarity index 100%
rename from internal/db/reset/templates/list.sql
rename to pkg/migration/queries/list.sql
diff --git a/pkg/storage/batch.go b/pkg/storage/batch.go
new file mode 100644
index 00000000..c4f0c83f
--- /dev/null
+++ b/pkg/storage/batch.go
@@ -0,0 +1,46 @@
+package storage
+
+import (
+	"context"
+	"fmt"
+	"os"
+)
+
+func (s *StorageAPI) UpsertBuckets(ctx context.Context, config map[string]BucketProps, filter ...func(string) bool) error {
+	buckets, err := s.ListBuckets(ctx)
+	if err != nil {
+		return err
+	}
+	exists := make(map[string]string, len(buckets))
+	for _, b := range buckets {
+		exists[b.Name] = b.Id
+	}
+	for name, props := range config {
+		// Update bucket properties if already exists
+		if bucketId, ok := exists[name]; ok {
+			for _, keep := range filter {
+				if !keep(bucketId) {
+					continue
+				}
+			}
+			fmt.Fprintln(os.Stderr, "Updating storage bucket:", bucketId)
+			body := UpdateBucketRequest{
+				Id:          bucketId,
+				BucketProps: &props,
+			}
+			if _, err := s.UpdateBucket(ctx, body); err != nil {
+				return err
+			}
+		} else {
+			fmt.Fprintln(os.Stderr, "Creating storage bucket:", name)
+			body := CreateBucketRequest{
+				Name:        name,
+				BucketProps: &props,
+			}
+			if _, err := s.CreateBucket(ctx, body); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go
index cadbd54d..d5e8c6a4 100644
--- a/internal/db/diff/diff_test.go
+++ b/internal/db/diff/diff_test.go
@@ -17,7 +17,6 @@ import (
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/db/start"
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/testing/fstest"
@@ -36,19 +35,6 @@ var dbConfig = pgconn.Config{
 	Database: "postgres",
 }
 
-var escapedSchemas = []string{
-	`\_analytics`,
-	`\_realtime`,
-	`\_supavisor`,
-	"pgbouncer",
-	"pgsodium",
-	"pgtle",
-	`supabase\_migrations`,
-	"vault",
-	`information\_schema`,
-	`pg\_%`,
-}
-
 func TestRun(t *testing.T) {
 	t.Run("runs migra diff", func(t *testing.T) {
 		// Setup in-memory fs
@@ -117,7 +103,7 @@ func TestRun(t *testing.T) {
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(reset.ListSchemas, escapedSchemas).
+		conn.Query(migration.ListSchemas, migration.ManagedSchemas).
 			ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`)
 		// Run test
 		err := Run(context.Background(), []string{}, "", dbConfig, DiffSchemaMigra, fsys, conn.Intercept)
diff --git a/internal/db/pull/pull_test.go b/internal/db/pull/pull_test.go
index 5c50e8f9..2e55fcfe 100644
--- a/internal/db/pull/pull_test.go
+++ b/internal/db/pull/pull_test.go
@@ -13,7 +13,6 @@ import (
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/supabase/cli/internal/db/reset"
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/testing/fstest"
 	"github.com/supabase/cli/internal/utils"
@@ -29,19 +28,6 @@ var dbConfig = pgconn.Config{
 	Database: "postgres",
 }
 
-var escapedSchemas = []string{
-	`\_analytics`,
-	`\_realtime`,
-	`\_supavisor`,
-	"pgbouncer",
-	"pgsodium",
-	"pgtle",
-	`supabase\_migrations`,
-	"vault",
-	`information\_schema`,
-	`pg\_%`,
-}
-
 func TestPullCommand(t *testing.T) {
 	t.Run("throws error on missing config", func(t *testing.T) {
 		// Setup in-memory fs
@@ -117,7 +103,7 @@ func TestPullSchema(t *testing.T) {
 		defer conn.Close(t)
 		conn.Query(migration.LIST_MIGRATION_VERSION).
 			Reply("SELECT 1", []interface{}{"0"}).
-			Query(reset.ListSchemas, escapedSchemas).
+			Query(migration.ListSchemas, migration.ManagedSchemas).
 			ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`)
 		// Run test
 		err := utils.RunProgram(context.Background(), func(p utils.Program, ctx context.Context) error {
diff --git a/internal/db/reset/reset_test.go b/internal/db/reset/reset_test.go
index 01d0e435..90b5ecd7 100644
--- a/internal/db/reset/reset_test.go
+++ b/internal/db/reset/reset_test.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -19,7 +20,9 @@ import (
 	"github.com/supabase/cli/internal/db/start"
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/testing/fstest"
+	"github.com/supabase/cli/internal/testing/helper"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/migration"
 	"github.com/supabase/cli/pkg/pgtest"
 )
 
@@ -315,20 +318,7 @@ func TestRestartDatabase(t *testing.T) {
 	})
 }
 
-var escapedSchemas = []string{
-	"extensions",
-	"public",
-	`\_analytics`,
-	`\_realtime`,
-	`\_supavisor`,
-	"pgbouncer",
-	"pgsodium",
-	"pgtle",
-	`supabase\_migrations`,
-	"vault",
-	`information\_schema`,
-	`pg\_%`,
-}
+var escapedSchemas = append(migration.ManagedSchemas, "extensions", "public")
 
 func TestResetRemote(t *testing.T) {
 	dbConfig := pgconn.Config{
@@ -342,15 +332,20 @@ func TestResetRemote(t *testing.T) {
 	t.Run("resets remote database", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
+		path := filepath.Join(utils.MigrationsDir, "0_schema.sql")
+		require.NoError(t, afero.WriteFile(fsys, path, nil, 0644))
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(ListSchemas, escapedSchemas).
+		conn.Query(migration.ListSchemas, escapedSchemas).
 			Reply("SELECT 1", []interface{}{"private"}).
 			Query("DROP SCHEMA IF EXISTS private CASCADE").
 			Reply("DROP SCHEMA").
-			Query(dropObjects).
+			Query(migration.DropObjects).
 			Reply("INSERT 0")
+		helper.MockMigrationHistory(conn).
+			Query(migration.INSERT_MIGRATION_VERSION, "0", "schema", nil).
+			Reply("INSERT 0 1")
 		// Run test
 		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
 		// Check error
@@ -366,29 +361,15 @@ func TestResetRemote(t *testing.T) {
 		assert.ErrorContains(t, err, "invalid port (outside range)")
 	})
 
-	t.Run("throws error on list schema failure", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup mock postgres
-		conn := pgtest.NewConn()
-		defer conn.Close(t)
-		conn.Query(ListSchemas, escapedSchemas).
-			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation information_schema")
-		// Run test
-		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
-		// Check error
-		assert.ErrorContains(t, err, "ERROR: permission denied for relation information_schema (SQLSTATE 42501)")
-	})
-
 	t.Run("throws error on drop schema failure", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup mock postgres
 		conn := pgtest.NewConn()
 		defer conn.Close(t)
-		conn.Query(ListSchemas, escapedSchemas).
+		conn.Query(migration.ListSchemas, escapedSchemas).
 			Reply("SELECT 0").
-			Query(dropObjects).
+			Query(migration.DropObjects).
 			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations")
 		// Run test
 		err := resetRemote(context.Background(), "", dbConfig, fsys, conn.Intercept)
diff --git a/internal/seed/buckets/buckets_test.go b/internal/seed/buckets/buckets_test.go
new file mode 100644
index 00000000..1d0e5f5e
--- /dev/null
+++ b/internal/seed/buckets/buckets_test.go
@@ -0,0 +1,61 @@
+package buckets
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/BurntSushi/toml"
+	"github.com/h2non/gock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/supabase/cli/internal/testing/apitest"
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/storage"
+)
+
+func TestSeedBuckets(t *testing.T) {
+	t.Run("seeds buckets", func(t *testing.T) {
+		t.Cleanup(func() { clear(utils.Config.Storage.Buckets) })
+		config := `
+[test]
+public = true
+[private]
+public = false`
+		require.NoError(t, toml.Unmarshal([]byte(config), &utils.Config.Storage.Buckets))
+		// Setup mock api
+		gock.New(utils.Config.Api.ExternalUrl).
+			Get("/storage/v1/bucket").
+			Reply(http.StatusOK).
+			JSON([]storage.BucketResponse{{
+				Name: "test",
+				Id:   "test",
+			}})
+		gock.New(utils.Config.Api.ExternalUrl).
+			Put("/storage/v1/bucket/test").
+			Reply(http.StatusOK).
+			JSON(storage.UpdateBucketResponse{})
+		gock.New(utils.Config.Api.ExternalUrl).
+			Post("/storage/v1/bucket").
+			Reply(http.StatusOK).
+			JSON(storage.CreateBucketResponse{Name: "private"})
+		// Run test
+		err := Run(context.Background(), "", utils.NewConsole())
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("ignores unconfigured buckets", func(t *testing.T) {
+		// Setup mock api
+		gock.New(utils.Config.Api.ExternalUrl).
+			Get("/storage/v1/bucket").
+			Reply(http.StatusOK).
+			JSON([]storage.BucketResponse{})
+		// Run test
+		err := Run(context.Background(), "", nil)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}
diff --git a/internal/start/start_test.go b/internal/start/start_test.go
index 4b4b0eaa..645a1cd7 100644
--- a/internal/start/start_test.go
+++ b/internal/start/start_test.go
@@ -133,8 +133,6 @@ func TestDatabaseStart(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.Image), utils.StorageId)
 		utils.ImgProxyId = "test-imgproxy"
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Storage.ImageTransformation.Image), utils.ImgProxyId)
-		// utils.DifferId = "test-differ"
-		// apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.DifferImage), utils.DifferId)
 		utils.EdgeRuntimeId = "test-edge-runtime"
 		apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), utils.EdgeRuntimeId)
 		utils.PgmetaId = "test-pgmeta"
diff --git a/internal/utils/deno_test.go b/internal/utils/deno_test.go
index fa12c13a..7105fc31 100644
--- a/internal/utils/deno_test.go
+++ b/internal/utils/deno_test.go
@@ -111,6 +111,7 @@ func TestImportMapPath(t *testing.T) {
 	})
 
 	t.Run("per function config takes precedence", func(t *testing.T) {
+		t.Cleanup(func() { clear(Config.Functions) })
 		slug := "hello"
 		config := fmt.Sprintf("[%s]\n", slug)
 		config += `import_map = "import_map.json"`
@@ -125,6 +126,7 @@ func TestImportMapPath(t *testing.T) {
 	})
 
 	t.Run("overrides with cli flag", func(t *testing.T) {
+		t.Cleanup(func() { clear(Config.Functions) })
 		slug := "hello"
 		config := fmt.Sprintf("[%s]\n", slug)
 		config += `import_map = "import_map.json"`
diff --git a/pkg/migration/drop_test.go b/pkg/migration/drop_test.go
new file mode 100644
index 00000000..d2c25ed5
--- /dev/null
+++ b/pkg/migration/drop_test.go
@@ -0,0 +1,56 @@
+package migration
+
+import (
+	"context"
+	"testing"
+
+	"github.com/jackc/pgerrcode"
+	"github.com/stretchr/testify/assert"
+	"github.com/supabase/cli/pkg/pgtest"
+)
+
+var escapedSchemas = append(ManagedSchemas, "extensions", "public")
+
+func TestDropSchemas(t *testing.T) {
+	t.Run("resets remote database", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(ListSchemas, escapedSchemas).
+			Reply("SELECT 1", []interface{}{"private"}).
+			Query("DROP SCHEMA IF EXISTS private CASCADE").
+			Reply("DROP SCHEMA").
+			Query(DropObjects).
+			Reply("INSERT 0")
+		// Run test
+		err := DropUserSchemas(context.Background(), conn.MockClient(t))
+		// Check error
+		assert.NoError(t, err)
+	})
+
+	t.Run("throws error on list schema failure", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(ListSchemas, escapedSchemas).
+			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation information_schema")
+		// Run test
+		err := DropUserSchemas(context.Background(), conn.MockClient(t))
+		// Check error
+		assert.ErrorContains(t, err, "ERROR: permission denied for relation information_schema (SQLSTATE 42501)")
+	})
+
+	t.Run("throws error on drop schema failure", func(t *testing.T) {
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(ListSchemas, escapedSchemas).
+			Reply("SELECT 0").
+			Query(DropObjects).
+			ReplyError(pgerrcode.InsufficientPrivilege, "permission denied for relation supabase_migrations")
+		// Run test
+		err := DropUserSchemas(context.Background(), conn.MockClient(t))
+		// Check error
+		assert.ErrorContains(t, err, "ERROR: permission denied for relation supabase_migrations (SQLSTATE 42501)")
+	})
+}