Skip to content

Commit f7b79ef

Browse files
authored
fix: add schema qualification hints for extension type errors (#4513)
1 parent fe61171 commit f7b79ef

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

pkg/migration/file.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ type MigrationFile struct {
2626
Statements []string
2727
}
2828

29-
var migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`)
29+
var (
30+
migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`)
31+
typeNamePattern = regexp.MustCompile(`type "([^"]+)" does not exist`)
32+
)
3033

3134
func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) {
3235
lines, err := parseFile(path, fsys)
@@ -96,6 +99,14 @@ func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error {
9699
if len(pgErr.Detail) > 0 {
97100
msg = append(msg, pgErr.Detail)
98101
}
102+
// Provide helpful hint for extension type errors (SQLSTATE 42704: undefined_object)
103+
if typeName := extractTypeName(pgErr.Message); len(typeName) > 0 && pgErr.Code == "42704" && !IsSchemaQualified(typeName) {
104+
msg = append(msg, "")
105+
msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.")
106+
msg = append(msg, " Use schema-qualified type references to avoid this error:")
107+
msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName))
108+
msg = append(msg, " Learn more: supabase migration new --help")
109+
}
99110
}
100111
msg = append(msg, fmt.Sprintf("At statement: %d", i), stat)
101112
return errors.Errorf("%w\n%s", err, strings.Join(msg, "\n"))
@@ -120,6 +131,21 @@ func markError(stat string, pos int) string {
120131
return strings.Join(lines, "\n")
121132
}
122133

134+
// extractTypeName extracts the type name from PostgreSQL error messages like:
135+
// 'type "ltree" does not exist' -> "ltree"
136+
func extractTypeName(errMsg string) string {
137+
matches := typeNamePattern.FindStringSubmatch(errMsg)
138+
if len(matches) > 1 {
139+
return matches[1]
140+
}
141+
return ""
142+
}
143+
144+
// IsSchemaQualified checks if a type name already contains a schema qualifier (e.g., "extensions.ltree")
145+
func IsSchemaQualified(typeName string) bool {
146+
return strings.Contains(typeName, ".")
147+
}
148+
123149
func (m *MigrationFile) insertVersionSQL(conn *pgx.Conn, batch *pgconn.Batch) error {
124150
value := pgtype.TextArray{}
125151
if err := value.Set(m.Statements); err != nil {

pkg/migration/file_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,84 @@ func TestMigrationFile(t *testing.T) {
7777
assert.ErrorContains(t, err, "ERROR: schema \"public\" already exists (SQLSTATE 42P06)")
7878
assert.ErrorContains(t, err, "At statement: 0\ncreate schema public")
7979
})
80+
81+
t.Run("provides helpful hint for extension type errors", func(t *testing.T) {
82+
migration := MigrationFile{
83+
Statements: []string{"CREATE TABLE test (path ltree NOT NULL)"},
84+
Version: "0",
85+
}
86+
// Setup mock postgres
87+
conn := pgtest.NewConn()
88+
defer conn.Close(t)
89+
conn.Query(migration.Statements[0]).
90+
ReplyError("42704", `type "ltree" does not exist`).
91+
Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements).
92+
Reply("INSERT 0 1")
93+
// Run test
94+
err := migration.ExecBatch(context.Background(), conn.MockClient(t))
95+
// Check error
96+
assert.ErrorContains(t, err, `type "ltree" does not exist`)
97+
assert.ErrorContains(t, err, "Hint: This type may be defined in a schema")
98+
assert.ErrorContains(t, err, "extensions.ltree")
99+
assert.ErrorContains(t, err, "supabase migration new --help")
100+
assert.ErrorContains(t, err, "At statement: 0")
101+
})
102+
103+
t.Run("skips hint for schema-qualified type errors", func(t *testing.T) {
104+
migration := MigrationFile{
105+
Statements: []string{"CREATE TABLE test (path extensions.ltree NOT NULL)"},
106+
Version: "0",
107+
}
108+
// Setup mock postgres
109+
conn := pgtest.NewConn()
110+
defer conn.Close(t)
111+
conn.Query(migration.Statements[0]).
112+
ReplyError("42704", `type "extensions.ltree" does not exist`).
113+
Query(INSERT_MIGRATION_VERSION, "0", "", migration.Statements).
114+
Reply("INSERT 0 1")
115+
// Run test
116+
err := migration.ExecBatch(context.Background(), conn.MockClient(t))
117+
// Check error - should NOT contain hint since type is already schema-qualified
118+
assert.ErrorContains(t, err, `type "extensions.ltree" does not exist`)
119+
assert.NotContains(t, err.Error(), "Hint: This type may be defined in a schema")
120+
})
121+
}
122+
123+
func TestExtractTypeName(t *testing.T) {
124+
t.Run("extracts type name from standard error message", func(t *testing.T) {
125+
result := extractTypeName(`type "ltree" does not exist`)
126+
assert.Equal(t, "ltree", result)
127+
})
128+
129+
t.Run("extracts schema-qualified type name", func(t *testing.T) {
130+
result := extractTypeName(`type "extensions.ltree" does not exist`)
131+
assert.Equal(t, "extensions.ltree", result)
132+
})
133+
134+
t.Run("extracts type with underscores", func(t *testing.T) {
135+
result := extractTypeName(`type "my_custom_type" does not exist`)
136+
assert.Equal(t, "my_custom_type", result)
137+
})
138+
139+
t.Run("returns empty string for non-matching message", func(t *testing.T) {
140+
result := extractTypeName(`column "name" does not exist`)
141+
assert.Equal(t, "", result)
142+
})
143+
144+
t.Run("returns empty string for empty message", func(t *testing.T) {
145+
result := extractTypeName("")
146+
assert.Equal(t, "", result)
147+
})
148+
149+
t.Run("handles type names with numbers", func(t *testing.T) {
150+
result := extractTypeName(`type "type123" does not exist`)
151+
assert.Equal(t, "type123", result)
152+
})
153+
}
154+
155+
func TestIsSchemaQualified(t *testing.T) {
156+
assert.True(t, IsSchemaQualified("extensions.ltree"))
157+
assert.True(t, IsSchemaQualified("public.my_type"))
158+
assert.False(t, IsSchemaQualified("ltree"))
159+
assert.False(t, IsSchemaQualified(""))
80160
}

0 commit comments

Comments
 (0)