From e11b228765beca956c4065e935389f72f0f69e43 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 10 Apr 2026 00:01:55 -0600 Subject: [PATCH] fix(pgmigrate): handle 42P01 surfaced lazily at rows.Err() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pgx/v5's Conn.Query is lazy — when the queried table doesn't exist, the 42P01 error doesn't surface at Query() time, it surfaces at rows.Err() after the iteration loop. The original code only checked for 42P01 at the Query() site, so first-run migrations against an empty database failed with: reading rows: ERROR: relation "_migrations" does not exist (SQLSTATE 42P01) Apply the typed-error check at both sites via a shared helper. --- database/sqlmigrate/pgmigrate/pgmigrate.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/database/sqlmigrate/pgmigrate/pgmigrate.go b/database/sqlmigrate/pgmigrate/pgmigrate.go index 4996a0d..3b4258b 100644 --- a/database/sqlmigrate/pgmigrate/pgmigrate.go +++ b/database/sqlmigrate/pgmigrate/pgmigrate.go @@ -68,10 +68,14 @@ func (r *Migrator) execInTx(ctx context.Context, sql string) error { // Applied returns all applied migrations from the _migrations table. // Returns an empty slice if the table does not exist (PG error 42P01). +// +// Note: pgx.Conn.Query is lazy — when the table is missing, the 42P01 +// error may surface at rows.Err() rather than at Query(). Both sites +// must check for it. func (r *Migrator) Applied(ctx context.Context) ([]sqlmigrate.Migration, error) { rows, err := r.Conn.Query(ctx, "SELECT id, name FROM _migrations ORDER BY name") if err != nil { - if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "42P01" { + if isUndefinedTable(err) { return nil, nil } return nil, fmt.Errorf("%w: %w", sqlmigrate.ErrQueryApplied, err) @@ -87,8 +91,18 @@ func (r *Migrator) Applied(ctx context.Context) ([]sqlmigrate.Migration, error) applied = append(applied, a) } if err := rows.Err(); err != nil { + if isUndefinedTable(err) { + return nil, nil + } return nil, fmt.Errorf("%w: reading rows: %w", sqlmigrate.ErrQueryApplied, err) } return applied, nil } + +// isUndefinedTable reports whether err is PostgreSQL error 42P01 +// (undefined_table), which is what we get when _migrations doesn't exist yet. +func isUndefinedTable(err error) bool { + pgErr, ok := errors.AsType[*pgconn.PgError](err) + return ok && pgErr.Code == "42P01" +}