From 711463f50c5d258d242e52fc83602427c9883d51 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:05:41 +0530 Subject: [PATCH 1/4] feat: improve error messages for undefined extension types Inspect pgx errors for SQLSTATE 42704 (undefined_object) and provide helpful hints when extension types are not found. The error message now: - Detects 'type does not exist' errors - Extracts the type name from the error message - Suggests using schema-qualified references (e.g., extensions.ltree) - Provides a concrete example in the error output This addresses the issue where migrations work locally but fail remotely with opaque 'type does not exist' errors, making it clear to users that they should use schema-qualified type references instead of relying on search_path settings. --- pkg/migration/file.go | 26 +++++++++++++++++++++++ pkg/migration/file_test.go | 43 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/pkg/migration/file.go b/pkg/migration/file.go index fbd4a3b7f..0635d8646 100644 --- a/pkg/migration/file.go +++ b/pkg/migration/file.go @@ -96,6 +96,20 @@ func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error { if len(pgErr.Detail) > 0 { msg = append(msg, pgErr.Detail) } + // Provide helpful hint for extension type errors (SQLSTATE 42704: undefined_object) + if pgErr.Code == "42704" && strings.Contains(pgErr.Message, "type") && strings.Contains(pgErr.Message, "does not exist") { + // Extract type name from error message (e.g., 'type "ltree" does not exist') + typeName := extractTypeName(pgErr.Message) + msg = append(msg, "") + msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.") + msg = append(msg, " Use schema-qualified type references to avoid this error:") + if typeName != "" { + msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) + } else { + msg = append(msg, " CREATE TABLE example (col extensions.);") + } + msg = append(msg, " Learn more: supabase migration new --help") + } } msg = append(msg, fmt.Sprintf("At statement: %d", i), stat) return errors.Errorf("%w\n%s", err, strings.Join(msg, "\n")) @@ -120,6 +134,18 @@ func markError(stat string, pos int) string { return strings.Join(lines, "\n") } +// extractTypeName extracts the type name from PostgreSQL error messages like: +// 'type "ltree" does not exist' -> "ltree" +func extractTypeName(errMsg string) string { + // Match pattern: type "typename" does not exist + re := regexp.MustCompile(`type "([^"]+)" does not exist`) + matches := re.FindStringSubmatch(errMsg) + if len(matches) > 1 { + return matches[1] + } + return "" +} + func (m *MigrationFile) insertVersionSQL(conn *pgx.Conn, batch *pgconn.Batch) error { value := pgtype.TextArray{} if err := value.Set(m.Statements); err != nil { diff --git a/pkg/migration/file_test.go b/pkg/migration/file_test.go index 45bee71b6..cf68f2361 100644 --- a/pkg/migration/file_test.go +++ b/pkg/migration/file_test.go @@ -77,4 +77,47 @@ func TestMigrationFile(t *testing.T) { assert.ErrorContains(t, err, "ERROR: schema \"public\" already exists (SQLSTATE 42P06)") assert.ErrorContains(t, err, "At statement: 0\ncreate schema public") }) + + t.Run("provides helpful hint for extension type errors", func(t *testing.T) { + migration := MigrationFile{ + Statements: []string{"CREATE TABLE test (path ltree NOT NULL)"}, + Version: "0", + } + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + conn.Query(migration.Statements[0]). + ReplyError("42704", `type "ltree" does not exist`). + Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). + Reply("INSERT 0 1") + // Run test + err := migration.ExecBatch(context.Background(), conn.MockClient(t)) + // Check error + assert.ErrorContains(t, err, `type "ltree" does not exist`) + assert.ErrorContains(t, err, "Hint: This type may be defined in a schema") + assert.ErrorContains(t, err, "extensions.ltree") + assert.ErrorContains(t, err, "supabase migration new --help") + assert.ErrorContains(t, err, "At statement: 0") + }) + + t.Run("provides generic hint when type name cannot be extracted", func(t *testing.T) { + migration := MigrationFile{ + Statements: []string{"CREATE TABLE test (id custom_type)"}, + Version: "0", + } + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + conn.Query(migration.Statements[0]). + ReplyError("42704", `type does not exist`). + Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements). + Reply("INSERT 0 1") + // Run test + err := migration.ExecBatch(context.Background(), conn.MockClient(t)) + // Check error + assert.ErrorContains(t, err, "type does not exist") + assert.ErrorContains(t, err, "Hint: This type may be defined in a schema") + assert.ErrorContains(t, err, "extensions.") + assert.ErrorContains(t, err, "supabase migration new --help") + }) } From 2f4e0f595f773c63817a370385718eb777332981 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:43:51 +0530 Subject: [PATCH 2/4] chore: update generated API types --- pkg/api/types.gen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 9483eddbd..5dd91424c 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -3407,7 +3407,7 @@ type UpdateApiKeyBody struct { type UpdateAuthConfigBody struct { ApiMaxRequestDuration nullable.Nullable[int] `json:"api_max_request_duration,omitempty"` DbMaxPoolSize nullable.Nullable[int] `json:"db_max_pool_size,omitempty"` - DbMaxPoolSizeUnit nullable.Nullable[UpdateAuthConfigBodyDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit"` + DbMaxPoolSizeUnit nullable.Nullable[UpdateAuthConfigBodyDbMaxPoolSizeUnit] `json:"db_max_pool_size_unit,omitempty"` DisableSignup nullable.Nullable[bool] `json:"disable_signup,omitempty"` ExternalAnonymousUsersEnabled nullable.Nullable[bool] `json:"external_anonymous_users_enabled,omitempty"` ExternalAppleAdditionalClientIds nullable.Nullable[string] `json:"external_apple_additional_client_ids,omitempty"` From fc49132e1b9e6966f6f6c679873af6759f05f1e6 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:09:45 +0530 Subject: [PATCH 3/4] Apply suggestion from @sweatybridge Co-authored-by: Han Qiao --- pkg/migration/file.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pkg/migration/file.go b/pkg/migration/file.go index 0635d8646..442b15d82 100644 --- a/pkg/migration/file.go +++ b/pkg/migration/file.go @@ -97,17 +97,11 @@ func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error { msg = append(msg, pgErr.Detail) } // Provide helpful hint for extension type errors (SQLSTATE 42704: undefined_object) - if pgErr.Code == "42704" && strings.Contains(pgErr.Message, "type") && strings.Contains(pgErr.Message, "does not exist") { - // Extract type name from error message (e.g., 'type "ltree" does not exist') - typeName := extractTypeName(pgErr.Message) + if typeName := extractTypeName(pgErr.Message); len(typeName) > 0 && pgErr.Code == "42704" { msg = append(msg, "") msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.") msg = append(msg, " Use schema-qualified type references to avoid this error:") - if typeName != "" { - msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) - } else { - msg = append(msg, " CREATE TABLE example (col extensions.);") - } + msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) msg = append(msg, " Learn more: supabase migration new --help") } } From 97072882af2c3da933a0f03f4001ddde97492ba3 Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:14:36 +0530 Subject: [PATCH 4/4] perf: move regex compilation to package level --- pkg/migration/file.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/migration/file.go b/pkg/migration/file.go index 442b15d82..2294810a0 100644 --- a/pkg/migration/file.go +++ b/pkg/migration/file.go @@ -26,7 +26,10 @@ type MigrationFile struct { Statements []string } -var migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) +var ( + migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) + typeNamePattern = regexp.MustCompile(`type "([^"]+)" does not exist`) +) func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) { lines, err := parseFile(path, fsys) @@ -131,9 +134,7 @@ func markError(stat string, pos int) string { // extractTypeName extracts the type name from PostgreSQL error messages like: // 'type "ltree" does not exist' -> "ltree" func extractTypeName(errMsg string) string { - // Match pattern: type "typename" does not exist - re := regexp.MustCompile(`type "([^"]+)" does not exist`) - matches := re.FindStringSubmatch(errMsg) + matches := typeNamePattern.FindStringSubmatch(errMsg) if len(matches) > 1 { return matches[1] }