Files
kandev/apps/backend/internal/db/migration_errors.go
T
Henry 4c0d92154b feat(backend): add per-run cost/token ledger for task usage (#2960)
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-08-23 22:15:19 +00:00

115 lines
3.5 KiB
Go

package db
import (
"errors"
"strings"
"github.com/jackc/pgx/v5/pgconn"
)
const (
postgresDuplicateColumn = "42701"
postgresDuplicateTable = "42P07"
postgresDuplicateObject = "42710"
postgresUndefinedTable = "42P01"
postgresForeignKeyViolation = "23503"
postgresSerializationFailure = "40001"
postgresDeadlockDetected = "40P01"
sqliteAlreadyExistsText = " already exists"
sqliteForeignKeyViolationMsg = "FOREIGN KEY constraint failed"
sqliteBusyText = "database is locked"
sqliteLockedText = "database table is locked"
)
// IsDuplicateColumnError reports whether err means an ADD COLUMN migration has
// already been applied.
func IsDuplicateColumnError(err error) bool {
if err == nil {
return false
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code == postgresDuplicateColumn
}
return strings.Contains(err.Error(), "duplicate column name")
}
// IsAlreadyExistsError reports whether err means a schema object already
// exists and the migration can be treated as an idempotent replay.
func IsAlreadyExistsError(err error) bool {
if err == nil {
return false
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case postgresDuplicateColumn, postgresDuplicateTable, postgresDuplicateObject:
return true
default:
return false
}
}
s := err.Error()
return strings.Contains(s, "duplicate column name") ||
isSQLiteDuplicateObjectMessage(s)
}
func isSQLiteDuplicateObjectMessage(s string) bool {
return strings.HasPrefix(s, "table ") && strings.Contains(s, sqliteAlreadyExistsText) ||
strings.HasPrefix(s, "index ") && strings.Contains(s, sqliteAlreadyExistsText) ||
strings.HasPrefix(s, "trigger ") && strings.Contains(s, sqliteAlreadyExistsText) ||
strings.HasPrefix(s, "view ") && strings.Contains(s, sqliteAlreadyExistsText)
}
// IsMissingTableError reports whether err means a query referenced a table
// that does not exist in this database — e.g. a cross-domain JOIN against a
// table owned by another package's schema that hasn't been initialised yet
// (isolated unit tests that only set up one domain's tables). Callers use
// this to fall back to a query that doesn't need the missing table, rather
// than failing outright.
func IsMissingTableError(err error) bool {
if err == nil {
return false
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code == postgresUndefinedTable
}
return strings.Contains(err.Error(), "no such table")
}
// IsForeignKeyViolation reports whether err means a write failed because it
// referenced a row that does not exist (or no longer exists) in a
// foreign-keyed table.
func IsForeignKeyViolation(err error) bool {
if err == nil {
return false
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
return pgErr.Code == postgresForeignKeyViolation
}
return strings.Contains(err.Error(), sqliteForeignKeyViolationMsg)
}
// IsTransientError reports whether err means a write failed for a reason
// that is expected to clear on retry: a serialization failure or deadlock on
// PostgreSQL, or the single-writer connection being busy/locked on SQLite.
func IsTransientError(err error) bool {
if err == nil {
return false
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case postgresSerializationFailure, postgresDeadlockDetected:
return true
default:
return false
}
}
s := err.Error()
return strings.Contains(s, sqliteBusyText) || strings.Contains(s, sqliteLockedText)
}