Skip to content

Commit 1b972d2

Browse files
committed
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.
1 parent b341e64 commit 1b972d2

File tree

2 files changed

+69
-0
lines changed

2 files changed

+69
-0
lines changed

pkg/migration/file.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ func (m *MigrationFile) ExecBatch(ctx context.Context, conn *pgx.Conn) error {
9696
if len(pgErr.Detail) > 0 {
9797
msg = append(msg, pgErr.Detail)
9898
}
99+
// Provide helpful hint for extension type errors (SQLSTATE 42704: undefined_object)
100+
if pgErr.Code == "42704" && strings.Contains(pgErr.Message, "type") && strings.Contains(pgErr.Message, "does not exist") {
101+
// Extract type name from error message (e.g., 'type "ltree" does not exist')
102+
typeName := extractTypeName(pgErr.Message)
103+
msg = append(msg, "")
104+
msg = append(msg, "Hint: This type may be defined in a schema that's not in your search_path.")
105+
msg = append(msg, " Use schema-qualified type references to avoid this error:")
106+
if typeName != "" {
107+
msg = append(msg, fmt.Sprintf(" CREATE TABLE example (col extensions.%s);", typeName))
108+
} else {
109+
msg = append(msg, " CREATE TABLE example (col extensions.<type_name>);")
110+
}
111+
msg = append(msg, " Learn more: supabase migration new --help")
112+
}
99113
}
100114
msg = append(msg, fmt.Sprintf("At statement: %d", i), stat)
101115
return errors.Errorf("%w\n%s", err, strings.Join(msg, "\n"))
@@ -120,6 +134,18 @@ func markError(stat string, pos int) string {
120134
return strings.Join(lines, "\n")
121135
}
122136

137+
// extractTypeName extracts the type name from PostgreSQL error messages like:
138+
// 'type "ltree" does not exist' -> "ltree"
139+
func extractTypeName(errMsg string) string {
140+
// Match pattern: type "typename" does not exist
141+
re := regexp.MustCompile(`type "([^"]+)" does not exist`)
142+
matches := re.FindStringSubmatch(errMsg)
143+
if len(matches) > 1 {
144+
return matches[1]
145+
}
146+
return ""
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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,47 @@ 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("provides generic hint when type name cannot be extracted", func(t *testing.T) {
104+
migration := MigrationFile{
105+
Statements: []string{"CREATE TABLE test (id custom_type)"},
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 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
118+
assert.ErrorContains(t, err, "type does not exist")
119+
assert.ErrorContains(t, err, "Hint: This type may be defined in a schema")
120+
assert.ErrorContains(t, err, "extensions.<type_name>")
121+
assert.ErrorContains(t, err, "supabase migration new --help")
122+
})
80123
}

0 commit comments

Comments
 (0)