Repository: supabase/cli
Pull request #1049 (2023-04-28 19:04:58)

feat: delete db dump comments by default
## What kind of change does this PR introduce?

fixes https://github.com/supabase/cli/issues/1047

## What is the current behavior?

- a lot of unnecessary comments generated from pg_dump
- unclear how to increase scanner buffer size

## What is the new behavior?

- removes commented out lines from db dump by default
- adds `db dump --keep-comments` flag to maintain existing behaviour
- uses file length as the max buffer size by default
- prints help text when input token is longer than scanner buffer size
```bash
$ supabase db reset
...
bufio.Scanner: token too long
After statement 1:      BEGIN;
Try setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (default is 256KB)
```

## Additional context

Add any other context or screenshots.


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

* **68** of **70**   **(97.14%)**  changed or added relevant lines in **7** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.2%**) to **60.736%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/db.go](https://coveralls.io/builds/59586153/source?filename=cmd%2Fdb.go#L112) | 1 | 2 | 50.0%
| [internal/db/remote/commit/commit.go](https://coveralls.io/builds/59586153/source?filename=internal%2Fdb%2Fremote%2Fcommit%2Fcommit.go#L78) | 0 | 1 | 0.0%
<!-- | **Total:** | **68** | **70** | **97.14%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/59586153/badge)](https://coveralls.io/builds/59586153) |
| :-- | --: |
| Change from base [Build 4870405574](https://coveralls.io/builds/59564605): |  0.2% |
| Covered Lines: | 4342 |
| Relevant Lines: | 7149 |

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


Patch:
diff --git a/cmd/db.go b/cmd/db.go
index f51ce53c..ea27e656 100644
--- a/cmd/db.go
+++ b/cmd/db.go
@@ -96,8 +96,9 @@ var (
 		},
 	}
 
-	dataOnly bool
-	roleOnly bool
+	dataOnly     bool
+	roleOnly     bool
+	keepComments bool
 
 	dbDumpCmd = &cobra.Command{
 		Use:   "dump",
@@ -108,7 +109,7 @@ var (
 				return err
 			}
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return dump.Run(ctx, file, dbConfig, dataOnly, roleOnly, fsys)
+			return dump.Run(ctx, file, dbConfig, dataOnly, roleOnly, keepComments, fsys)
 		},
 	}
 
@@ -226,6 +227,7 @@ func init() {
 	dumpFlags := dbDumpCmd.Flags()
 	dumpFlags.BoolVar(&dataOnly, "data-only", false, "Dumps only data records.")
 	dumpFlags.BoolVar(&roleOnly, "role-only", false, "Dumps only cluster roles.")
+	dumpFlags.BoolVar(&keepComments, "keep-comments", false, "Keeps commented lines from pg_dump output.")
 	dbDumpCmd.MarkFlagsMutuallyExclusive("data-only", "role-only")
 	dumpFlags.StringVarP(&file, "file", "f", "", "File path to save the dumped contents.")
 	dumpFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
diff --git a/internal/db/diff/migra.go b/internal/db/diff/migra.go
index 3bcc5f67..a1663afc 100644
--- a/internal/db/diff/migra.go
+++ b/internal/db/diff/migra.go
@@ -168,26 +168,6 @@ func MigrateShadowDatabase(ctx context.Context, fsys afero.Fs, options ...func(*
 	return apply.MigrateDatabase(ctx, conn, fsys)
 }
 
-// Applies local migration scripts to a database.
-func ApplyMigrations(ctx context.Context, url string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
-	// Parse connection url
-	config, err := pgx.ParseConfig(url)
-	if err != nil {
-		return err
-	}
-	// Apply config overrides
-	for _, op := range options {
-		op(config)
-	}
-	// Connect to database
-	conn, err := pgx.ConnectConfig(ctx, config)
-	if err != nil {
-		return err
-	}
-	defer conn.Close(context.Background())
-	return apply.MigrateDatabase(ctx, conn, fsys)
-}
-
 // Diffs local database schema against shadow, dumps output to stdout.
 func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) {
 	env := []string{"SOURCE=" + source, "TARGET=" + target}
diff --git a/internal/db/dump/dump.go b/internal/db/dump/dump.go
index 9dc4af58..a9f14104 100644
--- a/internal/db/dump/dump.go
+++ b/internal/db/dump/dump.go
@@ -23,7 +23,7 @@ var (
 	dumpRoleScript string
 )
 
-func Run(ctx context.Context, path string, config pgconn.Config, dataOnly, roleOnly bool, fsys afero.Fs) error {
+func Run(ctx context.Context, path string, config pgconn.Config, dataOnly, roleOnly, keepComments bool, fsys afero.Fs) error {
 	// Initialise output stream
 	var outStream afero.File
 	if len(path) > 0 {
@@ -59,6 +59,11 @@ func Run(ctx context.Context, path string, config pgconn.Config, dataOnly, roleO
 		excludedSchemas = utils.InternalSchemas
 	}
 	// Run script in docker
+	cmd := []string{"bash", "-c", script, "--"}
+	if !keepComments {
+		// Delete comments when arg1 is set
+		cmd = append(cmd, "1")
+	}
 	if err := utils.DockerRunOnceWithConfig(
 		ctx,
 		container.Config{
@@ -73,7 +78,7 @@ func Run(ctx context.Context, path string, config pgconn.Config, dataOnly, roleO
 				"ALLOWED_CONFIGS=" + strings.Join(utils.AllowedConfigs, "|"),
 				"DB_URL=" + config.Database,
 			},
-			Cmd: []string{"bash", "-c", script},
+			Cmd: cmd,
 		},
 		container.HostConfig{
 			NetworkMode: container.NetworkMode("host"),
diff --git a/internal/db/dump/templates/dump_data.sh b/internal/db/dump/templates/dump_data.sh
index 2be090c9..09e931a4 100755
--- a/internal/db/dump/templates/dump_data.sh
+++ b/internal/db/dump/templates/dump_data.sh
@@ -16,7 +16,9 @@ pg_dump \
     --exclude-table "storage.migrations" \
     --exclude-table "supabase_functions.migrations" \
     --schema '*' \
-    --dbname "$DB_URL"
+    --dbname "$DB_URL" \
+| sed -E "${1:+/^--/d}" \
+| uniq
 
 # Reset session config generated by pg_dump
 echo "RESET ALL;"
diff --git a/internal/db/dump/templates/dump_role.sh b/internal/db/dump/templates/dump_role.sh
index 2a6f39bf..b874ab23 100755
--- a/internal/db/dump/templates/dump_role.sh
+++ b/internal/db/dump/templates/dump_role.sh
@@ -19,7 +19,9 @@ pg_dumpall \
 | sed -E "s/^CREATE ROLE \"($RESERVED_ROLES)\"/-- &/" \
 | sed -E "s/^ALTER ROLE \"($RESERVED_ROLES)\"/-- &/" \
 | sed -E "s/^-- (.* SET \"($ALLOWED_CONFIGS)\" .*)/\1/" \
-| sed -E "s/GRANT \".*\" TO \"($RESERVED_ROLES)\"/-- &/"
+| sed -E "s/GRANT \".*\" TO \"($RESERVED_ROLES)\"/-- &/" \
+| sed -E "${1:+/^--/d}" \
+| uniq
 
 # Reset session config generated by pg_dump
 echo "RESET ALL;"
diff --git a/internal/db/dump/templates/dump_schema.sh b/internal/db/dump/templates/dump_schema.sh
index 1c8fb38d..d528800f 100755
--- a/internal/db/dump/templates/dump_schema.sh
+++ b/internal/db/dump/templates/dump_schema.sh
@@ -24,7 +24,9 @@ pg_dump \
 | sed -E "s/^GRANT (.+) ON (.+) \"($EXCLUDED_SCHEMAS)\"/-- &/" \
 | sed -E "s/^REVOKE (.+) ON (.+) \"($EXCLUDED_SCHEMAS)\"/-- &/" \
 | sed -E 's/^CREATE POLICY "cron_job_/-- &/' \
-| sed -E 's/^ALTER TABLE "cron"/-- &/'
+| sed -E 's/^ALTER TABLE "cron"/-- &/' \
+| sed -E "${1:+/^--/d}" \
+| uniq
 
 # Reset session config generated by pg_dump
 echo "RESET ALL;"
diff --git a/internal/db/push/push.go b/internal/db/push/push.go
index ba32b7d2..ca402812 100644
--- a/internal/db/push/push.go
+++ b/internal/db/push/push.go
@@ -10,10 +10,10 @@ import (
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
+	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/migration/list"
 	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/parser"
 )
 
 var (
@@ -82,30 +82,10 @@ func getPendingMigrations(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) ([
 
 func pushMigration(ctx context.Context, conn *pgx.Conn, filename string, fsys afero.Fs) error {
 	fmt.Fprintln(os.Stderr, "Pushing migration "+utils.Bold(filename)+"...")
-	sql, err := fsys.Open(filepath.Join(utils.MigrationsDir, filename))
+	path := filepath.Join(utils.MigrationsDir, filename)
+	migration, err := apply.NewMigrationFromFile(path, fsys)
 	if err != nil {
 		return err
 	}
-	lines, err := parser.SplitAndTrim(sql)
-	if err != nil {
-		return err
-	}
-	batch := pgconn.Batch{}
-	for _, line := range lines {
-		batch.ExecParams(line, nil, nil, nil, nil)
-	}
-	// Insert into migration history
-	lines = append(lines, repair.INSERT_MIGRATION_VERSION)
-	version := utils.MigrateFilePattern.FindStringSubmatch(filename)[1]
-	repair.InsertVersionSQL(&batch, version)
-	// ExecBatch is implicitly transactional
-	if result, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil {
-		i := len(result)
-		var stat string
-		if i < len(lines) {
-			stat = lines[i]
-		}
-		return fmt.Errorf("%v\nAt statement %d: %s", err, i, utils.Aqua(stat))
-	}
-	return nil
+	return migration.ExecBatch(ctx, conn)
 }
diff --git a/internal/db/remote/commit/commit.go b/internal/db/remote/commit/commit.go
index 7340c2db..39d0227f 100644
--- a/internal/db/remote/commit/commit.go
+++ b/internal/db/remote/commit/commit.go
@@ -75,7 +75,7 @@ func fetchRemote(p utils.Program, ctx context.Context, schema []string, timestam
 		return err
 	} else if len(migrations) == 0 {
 		p.Send(utils.StatusMsg("Committing initial migration on remote database..."))
-		return dump.Run(ctx, path, config, false, false, fsys)
+		return dump.Run(ctx, path, config, false, false, false, fsys)
 	}
 
 	w := utils.StatusWriter{Program: p}
diff --git a/internal/db/reset/reset.go b/internal/db/reset/reset.go
index 3e9120e0..7b88a6ad 100644
--- a/internal/db/reset/reset.go
+++ b/internal/db/reset/reset.go
@@ -16,7 +16,6 @@ import (
 	"github.com/supabase/cli/internal/migration/apply"
 	"github.com/supabase/cli/internal/status"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/internal/utils/parser"
 )
 
 const SET_POSTGRES_ROLE = "SET ROLE postgres;"
@@ -102,24 +101,15 @@ func RecreateDatabase(ctx context.Context, options ...func(*pgx.ConnConfig)) err
 }
 
 func SeedDatabase(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
-	sql, err := fsys.Open(utils.SeedDataPath)
+	fmt.Fprintln(os.Stderr, "Seeding data "+utils.Bold(utils.SeedDataPath)+"...")
+	seed, err := apply.NewMigrationFromFile(utils.SeedDataPath, fsys)
 	if errors.Is(err, os.ErrNotExist) {
 		return nil
 	} else if err != nil {
 		return err
 	}
-	defer sql.Close()
-	fmt.Fprintln(os.Stderr, "Seeding data "+utils.Bold(utils.SeedDataPath)+"...")
-	lines, err := parser.SplitAndTrim(sql)
-	if err != nil {
-		return err
-	}
 	// Batch seed commands, safe to use statement cache
-	batch := pgx.Batch{}
-	for _, line := range lines {
-		batch.Queue(line)
-	}
-	return conn.SendBatch(ctx, &batch).Close()
+	return seed.ExecBatchWithCache(ctx, conn)
 }
 
 func DisconnectClients(ctx context.Context, conn *pgx.Conn) error {
diff --git a/internal/migration/apply/apply.go b/internal/migration/apply/apply.go
index 2e1f9d00..1a89b1dc 100644
--- a/internal/migration/apply/apply.go
+++ b/internal/migration/apply/apply.go
@@ -6,11 +6,14 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strconv"
 
 	"github.com/jackc/pgconn"
 	"github.com/jackc/pgx/v4"
 	"github.com/spf13/afero"
+	"github.com/spf13/viper"
 	"github.com/supabase/cli/internal/migration/list"
+	"github.com/supabase/cli/internal/migration/repair"
 	"github.com/supabase/cli/internal/utils"
 	"github.com/supabase/cli/internal/utils/parser"
 )
@@ -31,31 +34,90 @@ func MigrateDatabase(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
 
 func migrateUp(ctx context.Context, conn *pgx.Conn, filename string, fsys afero.Fs) error {
 	fmt.Fprintln(os.Stderr, "Applying migration "+utils.Bold(filename)+"...")
-	sql, err := fsys.Open(filepath.Join(utils.MigrationsDir, filename))
+	path := filepath.Join(utils.MigrationsDir, filename)
+	migration, err := NewMigrationFromFile(path, fsys)
 	if err != nil {
 		return err
 	}
-	defer sql.Close()
-	return BatchExecDDL(ctx, conn, sql)
+	// Skip inserting to migration history table
+	migration.version = ""
+	return migration.ExecBatch(ctx, conn)
 }
 
 func BatchExecDDL(ctx context.Context, conn *pgx.Conn, sql io.Reader) error {
-	lines, err := parser.SplitAndTrim(sql)
+	migration, err := NewMigrationFromReader(sql)
 	if err != nil {
 		return err
 	}
+	return migration.ExecBatch(ctx, conn)
+}
+
+type MigrationFile struct {
+	Lines   []string
+	version string
+}
+
+func NewMigrationFromFile(path string, fsys afero.Fs) (*MigrationFile, error) {
+	sql, err := fsys.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer sql.Close()
+	// Unless explicitly specified, Use file length as max buffer size
+	if !viper.IsSet("SCANNER_BUFFER_SIZE") {
+		if fi, err := sql.Stat(); err == nil {
+			viper.Set("SCANNER_BUFFER_SIZE", strconv.FormatInt(fi.Size(), 10))
+		}
+	}
+	file, err := NewMigrationFromReader(sql)
+	if err == nil {
+		// Parse version from file name
+		filename := filepath.Base(path)
+		matches := utils.MigrateFilePattern.FindStringSubmatch(filename)
+		if len(matches) > 1 {
+			file.version = matches[1]
+		}
+	}
+	return file, err
+}
+
+func NewMigrationFromReader(sql io.Reader) (*MigrationFile, error) {
+	lines, err := parser.SplitAndTrim(sql)
+	if err != nil {
+		return nil, err
+	}
+	return &MigrationFile{Lines: lines}, nil
+}
+
+func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error {
 	// Batch migration commands, without using statement cache
-	batch := pgconn.Batch{}
-	for _, line := range lines {
+	batch := &pgconn.Batch{}
+	for _, line := range m.Lines {
 		batch.ExecParams(line, nil, nil, nil, nil)
 	}
-	if result, err := conn.PgConn().ExecBatch(ctx, &batch).ReadAll(); err != nil {
+	// Insert into migration history
+	if len(m.version) > 0 {
+		repair.InsertVersionSQL(batch, m.version)
+	}
+	// ExecBatch is implicitly transactional
+	if result, err := conn.PgConn().ExecBatch(ctx, batch).ReadAll(); err != nil {
+		// Defaults to printing the last statement on error
+		stat := repair.INSERT_MIGRATION_VERSION
 		i := len(result)
-		var stat string
-		if i < len(lines) {
-			stat = lines[i]
+		if i < len(m.Lines) {
+			stat = m.Lines[i]
 		}
-		return fmt.Errorf("%v\nAt statement %d: %s", err, i, utils.Aqua(stat))
+		return fmt.Errorf("%w\nAt statement %d: %s", err, i, utils.Aqua(stat))
 	}
 	return nil
 }
+
+func (m *MigrationFile) ExecBatchWithCache(ctx context.Context, conn *pgx.Conn) error {
+	// Data statements don't mutate schemas, safe to use statement cache
+	batch := pgx.Batch{}
+	for _, line := range m.Lines {
+		batch.Queue(line)
+	}
+	// No need to track version here because there are no schema changes
+	return conn.SendBatch(ctx, &batch).Close()
+}
diff --git a/internal/utils/parser/token.go b/internal/utils/parser/token.go
index 04e6f887..4c4a9d2d 100644
--- a/internal/utils/parser/token.go
+++ b/internal/utils/parser/token.go
@@ -2,6 +2,7 @@ package parser
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -106,7 +107,10 @@ func Split(sql io.Reader, transform ...func(string) string) (stats []string, err
 	}
 	err = scanner.Err()
 	if err != nil {
-		err = fmt.Errorf("%v\nAfter statement %d: %s", err, len(stats), utils.Aqua(token))
+		err = fmt.Errorf("%w\nAfter statement %d: %s", err, len(stats), utils.Aqua(token))
+	}
+	if errors.Is(err, bufio.ErrTooLong) {
+		err = fmt.Errorf("%w\nTry setting SUPABASE_SCANNER_BUFFER_SIZE=5MB (default is %dKB)", err, MaxScannerCapacity>>10)
 	}
 	return stats, err
 }


Test patch:
diff --git a/internal/db/diff/migra_test.go b/internal/db/diff/migra_test.go
index 90e3c2e2..d4288b2f 100644
--- a/internal/db/diff/migra_test.go
+++ b/internal/db/diff/migra_test.go
@@ -133,61 +133,6 @@ func TestRunMigra(t *testing.T) {
 	})
 }
 
-func TestApplyMigrations(t *testing.T) {
-	const postgresUrl = "postgresql://postgres:password@localhost:5432/postgres"
-
-	t.Run("applies migrations from local directory", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup initial migration
-		migrations := map[string]string{
-			filepath.Join(utils.MigrationsDir, "20220727064247_init.sql"): "create table test",
-			filepath.Join(utils.MigrationsDir, "20220727064248_drop.sql"): "drop table test;\n-- ignore me",
-		}
-		for path, query := range migrations {
-			require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
-		}
-		// Setup mock postgres
-		conn := pgtest.NewConn()
-		defer conn.Close(t)
-		conn.Query("create table test").
-			Reply("SELECT 0").
-			Query("drop table test").
-			Reply("SELECT 0").
-			Query("-- ignore me").
-			Reply("")
-		// Run test
-		assert.NoError(t, ApplyMigrations(context.Background(), postgresUrl, fsys, conn.Intercept))
-	})
-
-	t.Run("throws error on invalid postgres url", func(t *testing.T) {
-		assert.Error(t, ApplyMigrations(context.Background(), "invalid", afero.NewMemMapFs()))
-	})
-
-	t.Run("throws error on failture to connect", func(t *testing.T) {
-		assert.Error(t, ApplyMigrations(context.Background(), postgresUrl, afero.NewMemMapFs()))
-	})
-
-	t.Run("throws error on failture to send batch", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup initial migration
-		name := "20220727064247_create_table.sql"
-		path := filepath.Join(utils.MigrationsDir, name)
-		query := "create table test"
-		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
-		// Setup mock postgres
-		conn := pgtest.NewConn()
-		defer conn.Close(t)
-		conn.Query(query).
-			ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`)
-		// Run test
-		err := ApplyMigrations(context.Background(), postgresUrl, fsys, conn.Intercept)
-		// Check error
-		assert.ErrorContains(t, err, "ERROR: relation \"test\" already exists (SQLSTATE 42P07)\nAt statement 0: create table test")
-	})
-}
-
 func TestMigrateShadow(t *testing.T) {
 	t.Run("throws error on timeout", func(t *testing.T) {
 		utils.Config.Db.ShadowPort = 54320
diff --git a/internal/db/dump/dump_test.go b/internal/db/dump/dump_test.go
index 65c62190..8826795f 100644
--- a/internal/db/dump/dump_test.go
+++ b/internal/db/dump/dump_test.go
@@ -35,7 +35,7 @@ func TestPullCommand(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world"))
 		// Run test
-		err := Run(context.Background(), "schema.sql", dbConfig, false, false, fsys)
+		err := Run(context.Background(), "schema.sql", dbConfig, false, false, false, fsys)
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -54,7 +54,7 @@ func TestPullCommand(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world"))
 		// Run test
-		err := Run(context.Background(), "", dbConfig, false, false, fsys)
+		err := Run(context.Background(), "", dbConfig, false, false, false, fsys)
 		// Check error
 		assert.NoError(t, err)
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -70,7 +70,7 @@ func TestPullCommand(t *testing.T) {
 			Get("/v" + utils.Docker.ClientVersion() + "/images").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := Run(context.Background(), "", dbConfig, false, false, fsys)
+		err := Run(context.Background(), "", dbConfig, false, false, false, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error running pg_dump on remote database: request returned Service Unavailable")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -85,7 +85,7 @@ func TestPullCommand(t *testing.T) {
 		apitest.MockDockerStart(utils.Docker, imageUrl, containerId)
 		require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "hello world"))
 		// Run test
-		err := Run(context.Background(), "schema.sql", dbConfig, false, false, fsys)
+		err := Run(context.Background(), "schema.sql", dbConfig, false, false, false, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
diff --git a/internal/migration/apply/apply_test.go b/internal/migration/apply/apply_test.go
new file mode 100644
index 00000000..e71b3b72
--- /dev/null
+++ b/internal/migration/apply/apply_test.go
@@ -0,0 +1,149 @@
+package apply
+
+import (
+	"bufio"
+	"context"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/jackc/pgerrcode"
+	"github.com/spf13/afero"
+	"github.com/spf13/viper"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/supabase/cli/internal/migration/repair"
+	"github.com/supabase/cli/internal/testing/pgtest"
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/internal/utils/parser"
+)
+
+func TestMigrateDatabase(t *testing.T) {
+	t.Run("applies local migration", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		path := filepath.Join(utils.MigrationsDir, "0_test.sql")
+		sql := "create schema public"
+		require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644))
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(sql).
+			Reply("CREATE SCHEMA")
+		// Connect to mock
+		ctx := context.Background()
+		mock, err := utils.ConnectLocalPostgres(ctx, "localhost", 5432, "postgres", conn.Intercept)
+		require.NoError(t, err)
+		defer mock.Close(ctx)
+		// Run test
+		err = MigrateDatabase(ctx, mock, fsys)
+		// Check error
+		assert.NoError(t, err)
+	})
+
+	t.Run("ignores empty local directory", func(t *testing.T) {
+		assert.NoError(t, MigrateDatabase(context.Background(), nil, afero.NewMemMapFs()))
+	})
+
+	t.Run("ignores outdated migrations", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup initial migration
+		name := "20211208000000_init.sql"
+		path := filepath.Join(utils.MigrationsDir, name)
+		query := "create table test"
+		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
+		// Run test
+		err := MigrateDatabase(context.Background(), nil, fsys)
+		// Check error
+		assert.NoError(t, err)
+	})
+
+	t.Run("throws error on write failure", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		err := MigrateDatabase(context.Background(), nil, afero.NewReadOnlyFs(fsys))
+		// Check error
+		assert.ErrorIs(t, err, os.ErrPermission)
+	})
+
+	t.Run("throws error on open failure", func(t *testing.T) {
+		path := filepath.Join(utils.MigrationsDir, "20220727064247_create_table.sql")
+		// Setup in-memory fs
+		fsys := &OpenErrorFs{DenyPath: path}
+		query := "create table test"
+		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
+		// Run test
+		err := MigrateDatabase(context.Background(), nil, fsys)
+		// Check error
+		assert.ErrorIs(t, err, os.ErrPermission)
+	})
+}
+
+type OpenErrorFs struct {
+	afero.MemMapFs
+	DenyPath string
+}
+
+func (m *OpenErrorFs) Open(name string) (afero.File, error) {
+	if strings.HasPrefix(name, m.DenyPath) {
+		return nil, fs.ErrPermission
+	}
+	return m.MemMapFs.Open(name)
+}
+
+func TestMigrationFile(t *testing.T) {
+	t.Run("new from file sets max token", func(t *testing.T) {
+		viper.Reset()
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup initial migration
+		name := "20220727064247_create_table.sql"
+		path := filepath.Join(utils.MigrationsDir, name)
+		query := "BEGIN; " + strings.Repeat("a", parser.MaxScannerCapacity)
+		require.NoError(t, afero.WriteFile(fsys, path, []byte(query), 0644))
+		// Run test
+		migration, err := NewMigrationFromFile(path, fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Len(t, migration.Lines, 2)
+		assert.Equal(t, "20220727064247", migration.version)
+	})
+
+	t.Run("new from reader errors on max token", func(t *testing.T) {
+		viper.Reset()
+		sql := "\tBEGIN; " + strings.Repeat("a", parser.MaxScannerCapacity)
+		// Run test
+		migration, err := NewMigrationFromReader(strings.NewReader(sql))
+		// Check error
+		assert.ErrorIs(t, err, bufio.ErrTooLong)
+		assert.ErrorContains(t, err, "After statement 1: \tBEGIN;")
+		assert.Nil(t, migration)
+	})
+
+	t.Run("throws error on insert failure", func(t *testing.T) {
+		migration := MigrationFile{
+			Lines:   []string{"create schema public"},
+			version: "0",
+		}
+		// Setup mock postgres
+		conn := pgtest.NewConn()
+		defer conn.Close(t)
+		conn.Query(migration.Lines[0]).
+			ReplyError(pgerrcode.DuplicateSchema, `schema "public" already exists`).
+			Query(repair.INSERT_MIGRATION_VERSION, "0")
+		// Connect to mock
+		ctx := context.Background()
+		mock, err := utils.ConnectLocalPostgres(ctx, "localhost", 5432, "postgres", conn.Intercept)
+		require.NoError(t, err)
+		defer mock.Close(ctx)
+		// Run test
+		err = migration.ExecBatch(context.Background(), mock)
+		// Check error
+		assert.ErrorContains(t, err, "ERROR: schema \"public\" already exists (SQLSTATE 42P06)")
+		assert.ErrorContains(t, err, "At statement 0: create schema public")
+	})
+}
diff --git a/internal/utils/parser/token_test.go b/internal/utils/parser/token_test.go
index d3df8ba3..f0be87db 100644
--- a/internal/utils/parser/token_test.go
+++ b/internal/utils/parser/token_test.go
@@ -1,6 +1,7 @@
 package parser
 
 import (
+	"bufio"
 	_ "embed"
 	"io/fs"
 	"os"
@@ -44,6 +45,7 @@ func TestSplitAndTrim(t *testing.T) {
 	sql := "\tBEGIN; " + strings.Repeat("a", MaxScannerCapacity)
 	stats, err := SplitAndTrim(strings.NewReader(sql))
 	// Check error
-	assert.ErrorContains(t, err, "bufio.Scanner: token too long\nAfter statement 1: \tBEGIN;")
+	assert.ErrorIs(t, err, bufio.ErrTooLong)
+	assert.ErrorContains(t, err, "After statement 1: \tBEGIN;")
 	assert.ElementsMatch(t, []string{"BEGIN"}, stats)
 }