4c0d92154b
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>
115 lines
3.5 KiB
Go
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)
|
|
}
|