Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(errors): add unique constraint violation error #1069

Merged
merged 25 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading