Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
151 lines
4.8 KiB
Go
151 lines
4.8 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package sqlcon
|
|
|
|
import (
|
|
"database/sql"
|
|
stderrs "errors"
|
|
"net/http"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc/codes"
|
|
"modernc.org/sqlite"
|
|
|
|
"github.com/ory/herodot"
|
|
)
|
|
|
|
// ErrUniqueViolation is returned when a SQL INSERT / UPDATE command returns a conflict.
|
|
func ErrUniqueViolation() *herodot.DefaultError {
|
|
return &herodot.DefaultError{
|
|
CodeField: http.StatusConflict,
|
|
GRPCCodeField: codes.AlreadyExists,
|
|
StatusField: http.StatusText(http.StatusConflict),
|
|
ErrorField: "Unable to insert or update resource because a resource with that value exists already",
|
|
}
|
|
}
|
|
|
|
// ErrNoRows is returned when a SQL SELECT statement returns no rows.
|
|
func ErrNoRows() *herodot.DefaultError {
|
|
return &herodot.DefaultError{
|
|
CodeField: http.StatusNotFound,
|
|
GRPCCodeField: codes.NotFound,
|
|
StatusField: http.StatusText(http.StatusNotFound),
|
|
ErrorField: "Unable to locate the resource",
|
|
}
|
|
}
|
|
|
|
// ErrConcurrentUpdate is returned when the database is unable to serialize access due to a concurrent update.
|
|
func ErrConcurrentUpdate() *herodot.DefaultError {
|
|
return &herodot.DefaultError{
|
|
CodeField: http.StatusBadRequest,
|
|
GRPCCodeField: codes.Aborted,
|
|
StatusField: http.StatusText(http.StatusBadRequest),
|
|
ErrorField: "Unable to serialize access due to a concurrent update in another session",
|
|
}
|
|
}
|
|
|
|
func ErrNoSuchTable() *herodot.DefaultError {
|
|
return &herodot.DefaultError{
|
|
CodeField: http.StatusInternalServerError,
|
|
GRPCCodeField: codes.Internal,
|
|
StatusField: http.StatusText(http.StatusInternalServerError),
|
|
ErrorField: "Unable to locate the table",
|
|
}
|
|
}
|
|
|
|
func ErrNoSuchColumn() *herodot.DefaultError {
|
|
return &herodot.DefaultError{
|
|
CodeField: http.StatusInternalServerError,
|
|
GRPCCodeField: codes.Internal,
|
|
StatusField: http.StatusText(http.StatusInternalServerError),
|
|
ErrorField: "Unable to locate the column",
|
|
}
|
|
}
|
|
|
|
func handlePostgres(err error, sqlState string) error {
|
|
switch sqlState {
|
|
case "23505": // "unique_violation"
|
|
return errors.WithStack(ErrUniqueViolation().WithWrap(err))
|
|
case "40001", // "serialization_failure" in CRDB
|
|
"CR000": // "serialization_failure"
|
|
return errors.WithStack(ErrConcurrentUpdate().WithWrap(err))
|
|
case "42P01": // "no such table"
|
|
return errors.WithStack(ErrNoSuchTable().WithWrap(err))
|
|
case "42703": // "no such column"
|
|
return errors.WithStack(ErrNoSuchColumn().WithWrap(err))
|
|
}
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
// HandleError returns the right sqlcon.Err* depending on the input error.
|
|
func HandleError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
type stater = interface {
|
|
error
|
|
SQLState() string
|
|
}
|
|
|
|
if stderrs.Is(err, sql.ErrNoRows) {
|
|
return errors.WithStack(ErrNoRows())
|
|
}
|
|
if e, ok := stderrs.AsType[stater](err); ok {
|
|
return errors.WithStack(handlePostgres(err, e.SQLState()))
|
|
}
|
|
if e, ok := stderrs.AsType[*pgconn.PgError](err); ok {
|
|
return errors.WithStack(handlePostgres(err, e.Code))
|
|
}
|
|
if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok {
|
|
switch e.Number {
|
|
case 1062: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry
|
|
return errors.WithStack(ErrUniqueViolation().WithWrap(err))
|
|
case 1146: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_no_such_table
|
|
return errors.WithStack(ErrNoSuchTable().WithWrap(e))
|
|
case 1054: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_bad_field_error
|
|
return errors.WithStack(ErrNoSuchColumn().WithWrap(e))
|
|
case 1213: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_lock_deadlock
|
|
return errors.WithStack(ErrConcurrentUpdate().WithWrap(e))
|
|
}
|
|
}
|
|
if e, ok := stderrs.AsType[*sqlite.Error](err); ok {
|
|
return handleSqlite(e, err)
|
|
}
|
|
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
// IsForeignKeyViolation reports whether err is a foreign-key constraint
|
|
// violation, e.g. deleting a parent row that a child row still references
|
|
// under ON DELETE RESTRICT or NO ACTION.
|
|
func IsForeignKeyViolation(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
type stater = interface {
|
|
error
|
|
SQLState() string
|
|
}
|
|
|
|
if e, ok := stderrs.AsType[stater](err); ok {
|
|
return e.SQLState() == "23503" // "foreign_key_violation" in Postgres / CockroachDB
|
|
}
|
|
if e, ok := stderrs.AsType[*pgconn.PgError](err); ok {
|
|
return e.Code == "23503"
|
|
}
|
|
if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok {
|
|
// 1451: cannot delete or update a parent row (a foreign key constraint fails).
|
|
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_row_is_referenced_2
|
|
return e.Number == 1451
|
|
}
|
|
if e, ok := stderrs.AsType[*sqlite.Error](err); ok {
|
|
return isSqliteForeignKeyViolation(e)
|
|
}
|
|
return false
|
|
}
|