Skip to content

Commit

Permalink
feat(errors): add unique constraint violation error (#1069)
Browse files Browse the repository at this point in the history
  • Loading branch information
steebchen committed Jan 15, 2024
1 parent 9b77eb5 commit eba1a7f
Show file tree
Hide file tree
Showing 18 changed files with 366 additions and 31 deletions.
40 changes: 40 additions & 0 deletions docs/pages/docs/reference/client/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Errors

## ErrNotFound

`ErrNotFound` is returned when a query does not return any results. This error may be returned in `FindUnique`, `FindFirst`, but also when updating or deleting single records using `FindUnique().Update()` and `FindUnique().Delete()`.

```go
post, err := client.Post.FindFirst(
db.Post.Title.Equals("hi"),
).Exec(ctx)
if err != nil {
if errors.Is(err, db.ErrNotFound) {
panic("no record with title 'hi' found")
}
panic("error occurred: %s", err)
}
```

## IsUniqueConstraintViolation

A unique constraint violation happens when a query attempts to insert or update a record with a value that already exists in the database, or in other words, violates a unique constraint.

```go
user, err := db.User.CreateOne(...).Exec(cxt)
if err != nil {
if info, err := db.IsErrUniqueConstraint(err); err != nil {
// Fields exists for Postgres and SQLite
log.Printf("unique constraint on the fields: %s", info.Fields)

// you can also compare it with generated field names:
if info.Fields[0] == db.User.Name.Field() {
// do something
log.Printf("unique constraint on the `user.name` field")
}

// For MySQL and MongoDB, use the constraint key
log.Printf("unique constraint on the key: %s", info.Key)
}
}
```
4 changes: 2 additions & 2 deletions engine/mock/do.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"encoding/json"
"fmt"

"github.com/steebchen/prisma-client-go/engine"
"github.com/steebchen/prisma-client-go/engine/protocol"
)

func (e *Engine) Do(_ context.Context, payload interface{}, v interface{}) error {
Expand All @@ -16,7 +16,7 @@ func (e *Engine) Do(_ context.Context, payload interface{}, v interface{}) error

n := -1
for i, e := range expectations {
req := payload.(engine.GQLRequest)
req := payload.(protocol.GQLRequest)
str, err := e.Query.Build()
if err != nil {
return err
Expand Down
29 changes: 24 additions & 5 deletions engine/protocol.go → engine/protocol/protocol.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package engine
package protocol

import (
"encoding/json"
Expand Down Expand Up @@ -33,11 +33,30 @@ type GQLBatchRequest struct {
Transaction bool `json:"transaction"`
}

// GQLError is a GraphQL Error
type UserFacingError struct {
IsPanic bool `json:"is_panic"`
Message string `json:"message"`
Meta Meta `json:"meta"`
ErrorCode string `json:"error_code"`
}

func (e *UserFacingError) Error() string {
return e.Message
}

type Meta struct {
Target interface{} `json:"target"` // can be of type []string or string
}

// GQLError is a GraphQL Message
type GQLError struct {
Message string `json:"error"` // note: the query-engine uses 'error' instead of 'message'
Path []string `json:"path"`
Extensions map[string]interface{} `json:"query"`
Message string `json:"error"`
UserFacingError *UserFacingError `json:"user_facing_error"`
Path []string `json:"path"`
}

func (e *GQLError) Error() string {
return e.Message
}

func (e *GQLError) RawMessage() string {
Expand Down
3 changes: 2 additions & 1 deletion engine/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/steebchen/prisma-client-go/binaries"
"github.com/steebchen/prisma-client-go/engine/protocol"
"github.com/steebchen/prisma-client-go/logger"
"github.com/steebchen/prisma-client-go/runtime/types"
)
Expand Down Expand Up @@ -108,7 +109,7 @@ func (e *DataProxyEngine) Do(ctx context.Context, payload interface{}, into inte

startParse := time.Now()

var response GQLResponse
var response protocol.GQLResponse
if err := json.Unmarshal(body, &response); err != nil {
return fmt.Errorf("json gql resopnse unmarshal: %w", err)
}
Expand Down
16 changes: 11 additions & 5 deletions engine/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"time"

"github.com/steebchen/prisma-client-go/engine/protocol"
"github.com/steebchen/prisma-client-go/logger"
"github.com/steebchen/prisma-client-go/runtime/types"
)
Expand All @@ -28,18 +29,23 @@ func (e *QueryEngine) Do(ctx context.Context, payload interface{}, v interface{}

startParse := time.Now()

var response GQLResponse
var response protocol.GQLResponse
if err := json.Unmarshal(body, &response); err != nil {
return fmt.Errorf("json gql response unmarshal: %w", err)
}

if len(response.Errors) > 0 {
first := response.Errors[0]
if first.RawMessage() == internalUpdateNotFoundMessage ||
first.RawMessage() == internalDeleteNotFoundMessage {
e := response.Errors[0]
if e.RawMessage() == internalUpdateNotFoundMessage ||
e.RawMessage() == internalDeleteNotFoundMessage {
return types.ErrNotFound
}
return fmt.Errorf("pql error: %s", first.RawMessage())

if e.UserFacingError != nil {
return fmt.Errorf("user facing error: %w", e.UserFacingError)
}

return fmt.Errorf("internal error: %s", e.RawMessage())
}

response.Data.Result, err = transformResponse(response.Data.Result)
Expand Down
1 change: 1 addition & 0 deletions generator/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func generateClient(input *Root) error {
"client",
"enums",
"errors",
"fields",
"mock",
"models",
"query",
Expand Down
28 changes: 28 additions & 0 deletions generator/templates/errors.gotpl
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
{{- /*gotype:github.com/steebchen/prisma-client-go/generator.Root*/ -}}

var ErrNotFound = types.ErrNotFound
var IsErrNotFound = types.IsErrNotFound

type ErrUniqueConstraint = types.ErrUniqueConstraint[prismaFields]

// IsErrUniqueConstraint returns on a unique constraint error or violation with error info
// Use as follows:
//
// user, err := db.User.CreateOne(...).Exec(cxt)
// if err != nil {
// if info, err := db.IsErrUniqueConstraint(err); err != nil {
// // Fields exists for Postgres and SQLite
// log.Printf("unique constraint on the fields: %s", info.Fields)
//
// // you can also compare it with generated field names:
// if info.Fields[0] == db.User.Name.Field() {
// // do something
// }
//
// // For MySQL, use the constraint key
// log.Printf("unique constraint on the key: %s", info.Key)
// }
// }
//
func IsErrUniqueConstraint(err error) (*types.ErrUniqueConstraint[prismaFields], bool) {
return types.CheckUniqueConstraint[prismaFields](err)
}
11 changes: 11 additions & 0 deletions generator/templates/fields.gotpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{{- /*gotype:github.com/steebchen/prisma-client-go/generator.Root*/ -}}

type prismaFields string

{{ range $model := $.AST.Models }}
type {{ $model.Name.GoLowerCase }}PrismaFields = prismaFields

{{ range $field := $model.Fields }}
const {{ $model.Name.GoLowerCase }}Field{{ $field.Name.GoCase }} {{ $model.Name.GoLowerCase }}PrismaFields = "{{ $field.Name }}"
{{ end }}
{{ end }}
5 changes: 5 additions & 0 deletions generator/templates/query.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -452,5 +452,10 @@
}
{{ end }}
{{ end }}

{{/* Returns static field names */}}
func (r {{ $struct }}) Field() {{ $model.Name.GoLowerCase }}PrismaFields {
return {{ $model.Name.GoLowerCase }}Field{{ $field.Name.GoCase }}
}
{{ end }}
{{ end }}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
module github.com/steebchen/prisma-client-go

go 1.16
go 1.18

require (
github.com/joho/godotenv v1.5.1
github.com/shopspring/decimal v1.3.1
github.com/stretchr/testify v1.8.4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
Expand All @@ -7,15 +6,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
3 changes: 2 additions & 1 deletion runtime/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/steebchen/prisma-client-go/engine"
"github.com/steebchen/prisma-client-go/engine/protocol"
"github.com/steebchen/prisma-client-go/logger"
)

Expand Down Expand Up @@ -294,7 +295,7 @@ func (q Query) Exec(ctx context.Context, into interface{}) error {
if err != nil {
return err
}
payload := engine.GQLRequest{
payload := protocol.GQLRequest{
Query: str,
Variables: map[string]interface{}{},
}
Expand Down
11 changes: 6 additions & 5 deletions runtime/transaction/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"

"github.com/steebchen/prisma-client-go/engine"
"github.com/steebchen/prisma-client-go/engine/protocol"
"github.com/steebchen/prisma-client-go/runtime/builder"
)

Expand All @@ -27,17 +28,17 @@ func (r TX) Transaction(queries ...Param) Exec {
type Exec struct {
queries []Param
engine engine.Engine
requests []engine.GQLRequest
requests []protocol.GQLRequest
}

func (r Exec) Exec(ctx context.Context) error {
r.requests = make([]engine.GQLRequest, len(r.queries))
r.requests = make([]protocol.GQLRequest, len(r.queries))
for i, query := range r.queries {
str, err := query.ExtractQuery().Build()
if err != nil {
return err
}
r.requests[i] = engine.GQLRequest{
r.requests[i] = protocol.GQLRequest{
Query: str,
Variables: map[string]interface{}{},
}
Expand All @@ -48,8 +49,8 @@ func (r Exec) Exec(ctx context.Context) error {
defer close(q.ExtractQuery().TxResult)
}

var result engine.GQLBatchResponse
payload := engine.GQLBatchRequest{
var result protocol.GQLBatchResponse
payload := protocol.GQLBatchRequest{
Batch: r.requests,
Transaction: true,
}
Expand Down
65 changes: 64 additions & 1 deletion runtime/types/errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,69 @@
package types

import "errors"
import (
"errors"

"github.com/steebchen/prisma-client-go/engine/protocol"
)

// ErrNotFound gets returned when a database record does not exist
var ErrNotFound = errors.New("ErrNotFound")

// IsErrNotFound is true if the error is a ErrNotFound, which gets returned when a database record does not exist
// This can happen when you call `FindUnique` on a record, or update or delete a single record which doesn't exist.
func IsErrNotFound(err error) bool {
return errors.Is(err, ErrNotFound)
}

type F interface {
~string
}

type ErrUniqueConstraint[T F] struct {
// Message is the error message
Message string
// Fields only shows on Postgres
Fields []T
// Key only shows on MySQL
Key string
}

// CheckUniqueConstraint returns on a unique constraint error or violation with error info
// Ideally this will be replaced with Prisma-generated errors in the future
func CheckUniqueConstraint[T F](err error) (*ErrUniqueConstraint[T], bool) {
if err == nil {
return nil, false
}

var ufr *protocol.UserFacingError
if ok := errors.As(err, &ufr); !ok {
return nil, false
}

if ufr.ErrorCode != "P2002" {
return nil, false
}

// postgres
if items, ok := ufr.Meta.Target.([]interface{}); ok {
var fields []T
for _, f := range items {
field, ok := f.(string)
if ok {
fields = append(fields, T(field))
}
}
return &ErrUniqueConstraint[T]{
Fields: fields,
}, true
}

// mysql
if item, ok := ufr.Meta.Target.(string); ok {
return &ErrUniqueConstraint[T]{
Key: item,
}, true
}

return nil, false
}

0 comments on commit eba1a7f

Please sign in to comment.