-
Notifications
You must be signed in to change notification settings - Fork 372
Expand file tree
/
Copy patherrors.go
More file actions
135 lines (117 loc) · 4.71 KB
/
errors.go
File metadata and controls
135 lines (117 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package common
import (
"errors"
"regexp"
"slices"
"strings"
"github.com/jackc/pgx/v5/pgconn"
dscommon "github.com/authzed/spicedb/internal/datastore/common"
"github.com/authzed/spicedb/pkg/tuple"
)
const (
pgUniqueConstraintViolation = "23505"
pgSerializationFailure = "40001"
pgTransactionAborted = "25P02"
pgReadOnlyTransaction = "25006"
pgQueryCanceled = "57014"
pgInvalidArgument = "22023"
// PgMissingTable is the Postgres error code for "relation does not exist".
// This is used to detect when migrations have not been run.
PgMissingTable = "42P01"
)
var (
createConflictDetailsRegex = regexp.MustCompile(`^Key (.+)=\(([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\) already exists`)
createConflictDetailsRegexWithoutCaveat = regexp.MustCompile(`^Key (.+)=\(([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\) already exists`)
)
// IsQueryCanceledError returns true if the error is a Postgres error indicating a query was canceled.
func IsQueryCanceledError(err error) bool {
var pgerr *pgconn.PgError
return errors.As(err, &pgerr) && pgerr.Code == pgQueryCanceled
}
// IsConstraintFailureError returns true if the error is a Postgres error indicating a constraint
// failure.
func IsConstraintFailureError(err error) bool {
var pgerr *pgconn.PgError
return errors.As(err, &pgerr) && pgerr.Code == pgUniqueConstraintViolation
}
// IsReadOnlyTransactionError returns true if the error is a Postgres error indicating a read-only
// transaction.
func IsReadOnlyTransactionError(err error) bool {
var pgerr *pgconn.PgError
return errors.As(err, &pgerr) && pgerr.Code == pgReadOnlyTransaction
}
// IsSerializationFailureError returns true if the error is a Postgres error indicating a
// revision replication error.
func IsReplicationLagError(err error) bool {
var pgerr *pgconn.PgError
if errors.As(err, &pgerr) {
return (pgerr.Code == pgInvalidArgument && strings.Contains(pgerr.Message, "is in the future")) ||
strings.Contains(pgerr.Message, "replica missing revision") ||
IsSerializationError(err)
}
return false
}
// ConvertToWriteConstraintError converts the given Postgres error into a CreateRelationshipExistsError
// if applicable. If not applicable, returns nils.
func ConvertToWriteConstraintError(livingTupleConstraints []string, err error) error {
var pgerr *pgconn.PgError
if errors.As(err, &pgerr) && pgerr.Code == pgUniqueConstraintViolation && slices.Contains(livingTupleConstraints, pgerr.ConstraintName) {
found := createConflictDetailsRegex.FindStringSubmatch(pgerr.Detail)
if found != nil {
return dscommon.NewCreateRelationshipExistsError(&tuple.Relationship{
RelationshipReference: tuple.RelationshipReference{
Resource: tuple.ObjectAndRelation{
ObjectType: strings.TrimSpace(found[2]),
ObjectID: strings.TrimSpace(found[3]),
Relation: strings.TrimSpace(found[4]),
},
Subject: tuple.ObjectAndRelation{
ObjectType: strings.TrimSpace(found[5]),
ObjectID: strings.TrimSpace(found[6]),
Relation: strings.TrimSpace(found[7]),
},
},
})
}
found = createConflictDetailsRegexWithoutCaveat.FindStringSubmatch(pgerr.Detail)
if found != nil {
return dscommon.NewCreateRelationshipExistsError(&tuple.Relationship{
RelationshipReference: tuple.RelationshipReference{
Resource: tuple.ObjectAndRelation{
ObjectType: strings.TrimSpace(found[2]),
ObjectID: strings.TrimSpace(found[3]),
Relation: strings.TrimSpace(found[4]),
},
Subject: tuple.ObjectAndRelation{
ObjectType: strings.TrimSpace(found[5]),
ObjectID: strings.TrimSpace(found[6]),
Relation: strings.TrimSpace(found[7]),
},
},
})
}
return dscommon.NewCreateRelationshipExistsError(nil)
}
return nil
}
// IsMissingTableError returns true if the error is a Postgres error indicating a missing table.
// This typically happens when migrations have not been run.
func IsMissingTableError(err error) bool {
var pgerr *pgconn.PgError
return errors.As(err, &pgerr) && pgerr.Code == PgMissingTable
}
// WrapMissingTableError checks if the error is a missing table error and wraps it with
// a helpful message instructing the user to run migrations. If it's not a missing table error,
// it returns nil. If it's already a SchemaNotInitializedError, it returns the original error
// to preserve the wrapped error through the call chain.
func WrapMissingTableError(err error) error {
// Don't double-wrap if already a SchemaNotInitializedError - return original to preserve it
var schemaErr dscommon.SchemaNotInitializedError
if errors.As(err, &schemaErr) {
return err
}
if IsMissingTableError(err) {
return dscommon.NewSchemaNotInitializedError(err)
}
return nil
}