diff --git a/pkg/migration/file.go b/pkg/migration/file.go index fbd4a3b7f..540c129e3 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) @@ -96,6 +99,14 @@ 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 typeName := extractTypeName(pgErr.Message); len(typeName) > 0 && pgErr.Code == "42704" && !IsSchemaQualified(typeName) { + 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:") + msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName)) + 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 +131,21 @@ 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 { + matches := typeNamePattern.FindStringSubmatch(errMsg) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +// IsSchemaQualified checks if a type name already contains a schema qualifier (e.g., "extensions.ltree") +func IsSchemaQualified(typeName string) bool { + return strings.Contains(typeName, ".") +} + 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..bd090353f 100644 --- a/pkg/migration/file_test.go +++ b/pkg/migration/file_test.go @@ -77,4 +77,52 @@ 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("skips hint for schema-qualified type errors", func(t *testing.T) { + migration := MigrationFile{ + Statements: []string{"CREATE TABLE test (path extensions.ltree NOT NULL)"}, + Version: "0", + } + // Setup mock postgres + conn := pgtest.NewConn() + defer conn.Close(t) + conn.Query(migration.Statements[0]). + ReplyError("42704", `type "extensions.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 - should NOT contain hint since type is already schema-qualified + assert.ErrorContains(t, err, `type "extensions.ltree" does not exist`) + assert.NotContains(t, err.Error(), "Hint: This type may be defined in a schema") + }) +} + +func TestIsSchemaQualified(t *testing.T) { + assert.True(t, IsSchemaQualified("extensions.ltree")) + assert.True(t, IsSchemaQualified("public.my_type")) + assert.False(t, IsSchemaQualified("ltree")) + assert.False(t, IsSchemaQualified("")) }