diff --git a/CHANGELOG.md b/CHANGELOG.md index 112e8c4f6..476c89994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,21 @@ unreleased - Support `sslrootcert=system` and use `~/.postgresql/root.crt` as the default value of sslrootcert ([#1280], [#1281]). +- Add a new `pqerror` package with PostgreSQL error codes ([#1275]). + + For example, to test if an error is a UNIQUE constraint violation: + + if pqErr, ok := errors.AsType[*pq.Error](err); ok && pqErr.Code == pqerror.UniqueViolation { + log.Fatalf("email %q already exsts", email) + } + + To make this a bit more convenient, it also adds a `pq.As()` function: + + pqErr := pq.As(err, pqerror.UniqueViolation) + if pqErr != nil { + log.Fatalf("email %q already exsts", email) + } + ### Fixes - Fix SSL key permission check to allow modes stricter than 0600/0640#1265 ([#1265]). @@ -48,6 +63,7 @@ unreleased [#1270]: https://github.com/lib/pq/pull/1270 [#1271]: https://github.com/lib/pq/pull/1271 [#1272]: https://github.com/lib/pq/pull/1272 +[#1275]: https://github.com/lib/pq/pull/1275 [#1277]: https://github.com/lib/pq/pull/1277 [#1278]: https://github.com/lib/pq/pull/1278 [#1279]: https://github.com/lib/pq/pull/1279 @@ -167,6 +183,8 @@ newer. Previously PostgreSQL 8.4 and newer were supported. - Handle ErrorResponse in readReadyForQuery and return proper error ([#1136]). +- Detect COPY even if the query starts with whitespace or comments ([#1198]). + - CopyIn() and CopyInSchema() now work if the list of columns is empty, in which case it will copy all columns ([#1239]). @@ -192,6 +210,7 @@ newer. Previously PostgreSQL 8.4 and newer were supported. [#1180]: https://github.com/lib/pq/pull/1180 [#1184]: https://github.com/lib/pq/pull/1184 [#1188]: https://github.com/lib/pq/pull/1188 +[#1198]: https://github.com/lib/pq/pull/1198 [#1211]: https://github.com/lib/pq/pull/1211 [#1212]: https://github.com/lib/pq/pull/1212 [#1214]: https://github.com/lib/pq/pull/1214 diff --git a/README.md b/README.md index 5aedfa6c3..5ca523d42 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,17 @@ The libpq way (which also works in pq) is to use `options='-c k=v'` like so: Errors ------ -Errors from PostgreSQL are returned as [pq.Error]; the Error() string contains -the error message and code: +Errors from PostgreSQL are returned as [pq.Error]; [pq.As] can be used to +convert an error to `pq.Error`: + +```go +pqErr := pq.As(err, pqerror.UniqueViolation) +if pqErr != nil { + return fmt.Errorf("email %q already exsts", email) +} +``` + +the Error() string contains the error message and code: pq: duplicate key value violates unique constraint "users_lower_idx" (23505) @@ -118,6 +127,7 @@ It contains the context where this error occurred: ^ [pq.Error]: https://pkg.go.dev/github.com/lib/pq#Error +[pq.As]: https://pkg.go.dev/github.com/lib/pq#As PostgreSQL features ------------------- diff --git a/as.go b/as.go new file mode 100644 index 000000000..1ea0ee5bb --- /dev/null +++ b/as.go @@ -0,0 +1,26 @@ +//go:build !go1.26 + +package pq + +import ( + "errors" + "slices" +) + +// As asserts that the given error is [pq.Error] and returns it, returning nil +// if it's not a pq.Error. +// +// It will return nil if the pq.Error is not one of the given error codes. If no +// codes are given it will always return the Error. +// +// This is safe to call with a nil error. +func As(err error, codes ...ErrorCode) *Error { + if err == nil { // Not strictly needed, but prevents alloc for nil errors. + return nil + } + pqErr := new(Error) + if errors.As(err, &pqErr) && (len(codes) == 0 || slices.Contains(codes, pqErr.Code)) { + return pqErr + } + return nil +} diff --git a/as_go126.go b/as_go126.go new file mode 100644 index 000000000..18ffbc375 --- /dev/null +++ b/as_go126.go @@ -0,0 +1,23 @@ +//go:build go1.26 + +package pq + +import ( + "errors" + "github.com/lib/pq/pqerror" + "slices" +) + +// As asserts that the given error is [pq.Error] and returns it, returning nil +// if it's not a pq.Error. +// +// It will return nil if the pq.Error is not one of the given error codes. If no +// codes are given it will always return the Error. +// +// This is safe to call with a nil error. +func As(err error, codes ...pqerror.Code) *Error { + if pqErr, ok := errors.AsType[*Error](err); ok && (len(codes) == 0 || slices.Contains(codes, pqErr.Code)) { + return pqErr + } + return nil +} diff --git a/conn_test.go b/conn_test.go index 2f7e004b7..381c2d535 100644 --- a/conn_test.go +++ b/conn_test.go @@ -6,7 +6,6 @@ import ( "database/sql" "database/sql/driver" "encoding/json" - "errors" "fmt" "io" "math" @@ -22,6 +21,7 @@ import ( "github.com/lib/pq/internal/pqtest" "github.com/lib/pq/internal/pqutil" "github.com/lib/pq/internal/proto" + "github.com/lib/pq/pqerror" ) func TestReconnect(t *testing.T) { @@ -59,14 +59,14 @@ func TestReconnect(t *testing.T) { func TestCommitInFailedTransaction(t *testing.T) { db := pqtest.MustDB(t) - txn := pqtest.Begin(t, db) + tx := pqtest.Begin(t, db) - rows, err := txn.Query("SELECT error") + rows, err := tx.Query("select error") if err == nil { rows.Close() t.Fatal("expected failure") } - err = txn.Commit() + err = tx.Commit() if err != ErrInFailedTransaction { t.Fatalf("expected ErrInFailedTransaction; got %#v", err) } @@ -575,18 +575,7 @@ func TestErrorDuringStartup(t *testing.T) { // Don't use the normal connection setup, this is intended to blow up in the // startup packet from a non-existent user. _, err := pqtest.DB(t, "user=thisuserreallydoesntexist") - - if err == nil { - t.Fatal("expected error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("wrong error type %T: %[1]s", err) - } - if e.Code.Name() != "invalid_authorization_specification" && e.Code.Name() != "invalid_password" { - t.Fatalf("wrong error code %q: %s", e.Code.Name(), err) - } + mustAs(t, err, pqerror.InvalidAuthorizationSpecification, pqerror.InvalidPassword) } type testConn struct { @@ -642,7 +631,7 @@ func TestErrorDuringStartupClosesConn(t *testing.T) { func TestBadConn(t *testing.T) { t.Parallel() - for _, tt := range []error{io.EOF, &Error{Severity: Efatal}} { + for _, tt := range []error{io.EOF, &Error{Severity: pqerror.SeverityFatal}} { t.Run(fmt.Sprintf("%s", tt), func(t *testing.T) { var cn conn err := cn.handleError(tt) @@ -685,17 +674,8 @@ func TestConnClose(t *testing.T) { t.Fatal(err) } - // During the Go 1.9 cycle, https://github.com/golang/go/commit/3792db5 - // changed this error from - // - // net.errClosing = errors.New("use of closed network connection") - // - // to - // - // internal/poll.ErrClosing = errors.New("use of closed file or network connection") - const errClosing = "use of closed" - // Verify write after closing fails. + const errClosing = "use of closed" _, err = nc.Write(nil) if err == nil { t.Fatal("expected error") @@ -716,84 +696,33 @@ func TestConnClose(t *testing.T) { func TestErrorOnExec(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) - _, err = txn.Exec("INSERT INTO foo VALUES (0), (0)") - if err == nil { - t.Fatal("Should have raised error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + _, err := tx.Exec("insert into foo values (0), (0)") + mustAs(t, err, pqerror.UniqueViolation) } func TestErrorOnQuery(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } - - _, err = txn.Query("INSERT INTO foo VALUES (0), (0)") - if err == nil { - t.Fatal("Should have raised error") - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + _, err := tx.Query("insert into foo values (0), (0)") + mustAs(t, err, pqerror.UniqueViolation) } func TestErrorOnQueryRowSimpleQuery(t *testing.T) { db := pqtest.MustDB(t) + tx := pqtest.Begin(t, db) - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - defer txn.Rollback() - - _, err = txn.Exec("CREATE TEMPORARY TABLE foo(f1 int PRIMARY KEY)") - if err != nil { - t.Fatal(err) - } + pqtest.Exec(t, tx, `create temp table foo(f1 int primary key)`) var v int - err = txn.QueryRow("INSERT INTO foo VALUES (0), (0)").Scan(&v) - if err == nil { - t.Fatal("Should have raised error") - } - - e, ok := err.(*Error) - if !ok { - t.Fatalf("expected Error, got %#v", err) - } else if e.Code.Name() != "unique_violation" { - t.Fatalf("expected unique_violation, got %s (%+v)", e.Code.Name(), err) - } + err := tx.QueryRow("insert into foo values (0), (0)").Scan(&v) + mustAs(t, err, pqerror.UniqueViolation) } // Test the QueryRow bug workarounds in stmt.exec() and simpleQuery() @@ -807,53 +736,25 @@ func TestQueryRowBugWorkaround(t *testing.T) { } var a string - err = db.QueryRow("INSERT INTO notnulltemp(a) values($1) RETURNING a", nil).Scan(&a) - if err == sql.ErrNoRows { - t.Fatalf("expected constraint violation error; got: %v", err) - } - pge, ok := err.(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "not_null_violation" { - t.Fatalf("expected not_null_violation; got: %s (%+v)", pge.Code.Name(), err) - } + err = db.QueryRow("insert into notnulltemp(a) values($1) returning a", nil).Scan(&a) + mustAs(t, err, pqerror.NotNullViolation) // Test workaround in simpleQuery() - tx, err := db.Begin() - if err != nil { - t.Fatalf("unexpected error %s in Begin", err) - } - defer tx.Rollback() + tx := pqtest.Begin(t, db) - _, err = tx.Exec("SET LOCAL check_function_bodies TO FALSE") - if err != nil { - t.Fatalf("could not disable check_function_bodies: %s", err) - } - _, err = tx.Exec(` - CREATE OR REPLACE FUNCTION bad_function() - RETURNS integer + pqtest.Exec(t, tx, `set local check_function_bodies to false`) + pqtest.Exec(t, tx, ` + create or replace function bad_function() + returns integer -- hack to prevent the function from being inlined - SET check_function_bodies TO TRUE - AS $$ - SELECT text 'bad' - $$ LANGUAGE sql + set check_function_bodies to true + as $$ + select text 'bad' + $$ language sql `) - if err != nil { - t.Fatalf("could not create function: %s", err) - } - err = tx.QueryRow("SELECT * FROM bad_function()").Scan(&a) - if err == nil { - t.Fatalf("expected error") - } - pge, ok = err.(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "invalid_function_definition" { - t.Fatalf("expected invalid_function_definition; got: %s (%+v)", pge.Code.Name(), err) - } + err = tx.QueryRow("select * from bad_function()").Scan(&a) + mustAs(t, err, pqerror.InvalidFunctionDefinition) err = tx.Rollback() if err != nil { @@ -885,13 +786,7 @@ func TestQueryRowBugWorkaround(t *testing.T) { if rows.Next() { t.Fatalf("unexpected row") } - pge, ok = rows.Err().(*Error) - if !ok { - t.Fatalf("expected *Error; got: %#v", err) - } - if pge.Code.Name() != "cardinality_violation" { - t.Fatalf("expected cardinality_violation; got: %s (%+v)", pge.Code.Name(), rows.Err()) - } + mustAs(t, rows.Err(), pqerror.CardinalityViolation) } func TestSimpleQuery(t *testing.T) { @@ -963,14 +858,10 @@ func TestParseErrorInExtendedQuery(t *testing.T) { t.Parallel() db := pqtest.MustDB(t) - _, err := db.Query("PARSE_ERROR $1", 1) - pqErr, _ := err.(*Error) - // Expecting a syntax error. - if err == nil || pqErr == nil || pqErr.Code != "42601" { - t.Fatalf("expected syntax error, got %s", err) - } + _, err := db.Query("parse_error $1", 1) + mustAs(t, err, pqerror.SyntaxError) - rows, err := db.Query("SELECT 1") + rows, err := db.Query("select 1") if err != nil { t.Fatal(err) } @@ -987,8 +878,7 @@ func TestReturning(t *testing.T) { t.Fatal(err) } - rows, err := db.Query("INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') " + - "RETURNING did;") + rows, err := db.Query("INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets') RETURNING did;") if err != nil { t.Fatal(err) } @@ -1314,19 +1204,16 @@ func TestErrorClass(t *testing.T) { t.Parallel() db := pqtest.MustDB(t) - _, err := db.Query("SELECT int 'notint'") - if err == nil { + _, err := db.Query("select int 'notint'") + pqErr := As(err) + if pqErr == nil { t.Fatal("expected error") } - pge, ok := err.(*Error) - if !ok { - t.Fatalf("expected *pq.Error, got %#+v", err) + if pqErr.Code.Class() != "22" { + t.Fatalf("expected class 28, got %v", pqErr.Code.Class()) } - if pge.Code.Class() != "22" { - t.Fatalf("expected class 28, got %v", pge.Code.Class()) - } - if pge.Code.Class().Name() != "data_exception" { - t.Fatalf("expected data_exception, got %v", pge.Code.Class().Name()) + if pqErr.Code.Class().Name() != "data_exception" { + t.Fatalf("expected data_exception, got %v", pqErr.Code.Class().Name()) } } @@ -1534,31 +1421,26 @@ func TestConnPrepareContext(t *testing.T) { func TestStmtQueryContext(t *testing.T) { tests := []struct { - name string - ctx func() (context.Context, context.CancelFunc) - sql string - wantCancel bool + sql string + ctx func() (context.Context, context.CancelFunc) + wantErr string }{ - { - name: "context.WithTimeout exceeded", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(1)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 50*time.Millisecond) }, - sql: "select pg_sleep(1)", - wantCancel: true, + `pq: canceling statement due to user request (57014)`, }, - { - name: "context.WithTimeout", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(0.05)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Minute) }, - sql: "select pg_sleep(0.05)", - wantCancel: false, + ``, }, } for _, tt := range tests { tt := tt - t.Run(tt.name, func(t *testing.T) { + t.Run("", func(t *testing.T) { if !pqtest.Pgpool() { t.Parallel() } @@ -1566,20 +1448,15 @@ func TestStmtQueryContext(t *testing.T) { db := pqtest.MustDB(t) ctx, cancel := tt.ctx() - if cancel != nil { - defer cancel() - } + defer cancel() + stmt, err := db.PrepareContext(ctx, tt.sql) if err != nil { t.Fatal(err) } _, err = stmt.QueryContext(ctx) - pgErr := (*Error)(nil) - switch { - case (err != nil) != tt.wantCancel: - t.Fatalf("stmt.QueryContext() unexpected nil err got = %v, wantCancel = %v", err, tt.wantCancel) - case (err != nil && tt.wantCancel) && !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode): - t.Errorf("stmt.QueryContext() got = %v, wantCancel = %v", err.Error(), tt.wantCancel) + if !pqtest.ErrorContains(err, tt.wantErr) { + t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr) } }) } @@ -1587,31 +1464,26 @@ func TestStmtQueryContext(t *testing.T) { func TestStmtExecContext(t *testing.T) { tests := []struct { - name string - ctx func() (context.Context, context.CancelFunc) - sql string - wantCancel bool + sql string + ctx func() (context.Context, context.CancelFunc) + wantErr string }{ - { - name: "context.WithTimeout exceeded", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(1)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 50*time.Millisecond) }, - sql: "select pg_sleep(1)", - wantCancel: true, + `pq: canceling statement due to user request (57014)`, }, - { - name: "context.WithTimeout", - ctx: func() (context.Context, context.CancelFunc) { + {"select pg_sleep(0.05)", + func() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Minute) }, - sql: "select pg_sleep(0.05)", - wantCancel: false, + ``, }, } for _, tt := range tests { tt := tt - t.Run(tt.name, func(t *testing.T) { + t.Run("", func(t *testing.T) { if !pqtest.Pgpool() { t.Parallel() } @@ -1619,20 +1491,15 @@ func TestStmtExecContext(t *testing.T) { db := pqtest.MustDB(t) ctx, cancel := tt.ctx() - if cancel != nil { - defer cancel() - } + defer cancel() + stmt, err := db.PrepareContext(ctx, tt.sql) if err != nil { t.Fatal(err) } _, err = stmt.ExecContext(ctx) - pgErr := (*Error)(nil) - switch { - case (err != nil) != tt.wantCancel: - t.Fatalf("stmt.QueryContext() unexpected nil err got = %v, wantCancel = %v", err, tt.wantCancel) - case (err != nil && tt.wantCancel) && !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode): - t.Errorf("stmt.QueryContext() got = %v, wantCancel = %v", err.Error(), tt.wantCancel) + if !pqtest.ErrorContains(err, tt.wantErr) { + t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr) } }) } @@ -1712,11 +1579,8 @@ func TestContextCancelExec(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := db.ExecContext(ctx, "select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err := db.ExecContext(ctx, "select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Context is already canceled, so error should come before execution. if _, err := db.ExecContext(ctx, "select pg_sleep(1)"); err == nil { @@ -1751,11 +1615,8 @@ func TestContextCancelQuery(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := db.QueryContext(ctx, "select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err := db.QueryContext(ctx, "select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Context is already canceled, so error should come before execution. if _, err := db.QueryContext(ctx, "select pg_sleep(1)"); err == nil { @@ -1797,11 +1658,7 @@ func TestIssue617(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() _, err := db.QueryContext(ctx, `SELECT * FROM DOESNOTEXIST`) - pqErr, _ := err.(*Error) - // Expecting "pq: relation \"doesnotexist\" does not exist" error. - if err == nil || pqErr == nil || pqErr.Code != "42P01" { - t.Fatalf("expected undefined table error, got %v", err) - } + mustAs(t, err, pqerror.UndefinedTable) }() } @@ -1841,11 +1698,8 @@ func TestContextCancelBegin(t *testing.T) { defer time.AfterFunc(time.Millisecond*10, cancel).Stop() // Not canceled until after the exec has started. - if _, err := tx.Exec("select pg_sleep(1)"); err == nil { - t.Fatal("expected error") - } else if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("unexpected error: %s", err) - } + _, err = tx.Exec("select pg_sleep(1)") + mustAs(t, err, pqerror.QueryCanceled) // Transaction is canceled, so expect an error. if _, err := tx.Query("select pg_sleep(1)"); err == nil { @@ -1868,10 +1722,10 @@ func TestContextCancelBegin(t *testing.T) { cancel() if err != nil { t.Fatal(err) - } else if err, pgErr := tx.Rollback(), (*Error)(nil); err != nil && - !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) && - err != sql.ErrTxDone && err != driver.ErrBadConn && err != context.Canceled { - t.Fatal(err) + } + err = tx.Rollback() + if err != nil && err != sql.ErrTxDone && err != driver.ErrBadConn && err != context.Canceled { + mustAs(t, err, pqerror.QueryCanceled) } }() diff --git a/deprecated.go b/deprecated.go index 795d3328c..d43934a0a 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,6 +3,23 @@ package pq import ( "bytes" "database/sql" + + "github.com/lib/pq/pqerror" +) + +// [pq.Error.Severity] values. +// +// Deprecated: use pqerror.Severity[..] values. +// +//go:fix inline +const ( + Efatal = pqerror.SeverityFatal + Epanic = pqerror.SeverityPanic + Ewarning = pqerror.SeverityWarning + Enotice = pqerror.SeverityNotice + Edebug = pqerror.SeverityDebug + Einfo = pqerror.SeverityInfo + Elog = pqerror.SeverityLog ) // PGError is an interface used by previous versions of pq. diff --git a/error.go b/error.go index be704e923..0851a66be 100644 --- a/error.go +++ b/error.go @@ -9,22 +9,13 @@ import ( "strconv" "strings" "unicode/utf8" -) -// [pq.Error.Severity] values. -const ( - Efatal = "FATAL" - Epanic = "PANIC" - Ewarning = "WARNING" - Enotice = "NOTICE" - Edebug = "DEBUG" - Einfo = "INFO" - Elog = "LOG" + "github.com/lib/pq/pqerror" ) -// Error represents an error communicating with the server. +// Error returned by the PostgreSQL server. // -// The [Error] method only returns the error message and error code: +// The [Error] method returns the error message and error code: // // pq: invalid input syntax for type json (22P02) // @@ -39,15 +30,13 @@ const ( // 4 | 123, // 5 | 'foo', 'asd'::jsonb // ^ -// -// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields type Error struct { // [Efatal], [Epanic], [Ewarning], [Enotice], [Edebug], [Einfo], or [Elog]. // Always present. Severity string // SQLSTATE code. Always present. - Code ErrorCode + Code pqerror.Code // Primary human-readable error message. This should be accurate but terse // (typically one line). Always present. @@ -119,316 +108,21 @@ type Error struct { query string } -// ErrorCode is a five-character error code. -type ErrorCode string - -// Name returns a more human friendly rendering of the error code, namely the -// "condition name". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Name() string { - return errorCodeNames[ec] -} - -// ErrorClass is only the class part of an error code. -type ErrorClass string - -// Name returns the condition name of an error class. It is equivalent to the -// condition name of the "standard" error code (i.e. the one having the last -// three characters "000"). -func (ec ErrorClass) Name() string { - return errorCodeNames[ErrorCode(ec+"000")] -} - -// Class returns the error class, e.g. "28". -// -// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for -// details. -func (ec ErrorCode) Class() ErrorClass { - return ErrorClass(ec[0:2]) -} - -// errorCodeNames is a mapping between the five-character error codes and the -// human readable "condition names". It is derived from the list at -// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html -var errorCodeNames = map[ErrorCode]string{ - // Class 00 - Successful Completion - "00000": "successful_completion", - // Class 01 - Warning - "01000": "warning", - "0100C": "dynamic_result_sets_returned", - "01008": "implicit_zero_bit_padding", - "01003": "null_value_eliminated_in_set_function", - "01007": "privilege_not_granted", - "01006": "privilege_not_revoked", - "01004": "string_data_right_truncation", - "01P01": "deprecated_feature", - // Class 02 - No Data (this is also a warning class per the SQL standard) - "02000": "no_data", - "02001": "no_additional_dynamic_result_sets_returned", - // Class 03 - SQL Statement Not Yet Complete - "03000": "sql_statement_not_yet_complete", - // Class 08 - Connection Exception - "08000": "connection_exception", - "08003": "connection_does_not_exist", - "08006": "connection_failure", - "08001": "sqlclient_unable_to_establish_sqlconnection", - "08004": "sqlserver_rejected_establishment_of_sqlconnection", - "08007": "transaction_resolution_unknown", - "08P01": "protocol_violation", - // Class 09 - Triggered Action Exception - "09000": "triggered_action_exception", - // Class 0A - Feature Not Supported - "0A000": "feature_not_supported", - // Class 0B - Invalid Transaction Initiation - "0B000": "invalid_transaction_initiation", - // Class 0F - Locator Exception - "0F000": "locator_exception", - "0F001": "invalid_locator_specification", - // Class 0L - Invalid Grantor - "0L000": "invalid_grantor", - "0LP01": "invalid_grant_operation", - // Class 0P - Invalid Role Specification - "0P000": "invalid_role_specification", - // Class 0Z - Diagnostics Exception - "0Z000": "diagnostics_exception", - "0Z002": "stacked_diagnostics_accessed_without_active_handler", - // Class 20 - Case Not Found - "20000": "case_not_found", - // Class 21 - Cardinality Violation - "21000": "cardinality_violation", - // Class 22 - Data Exception - "22000": "data_exception", - "2202E": "array_subscript_error", - "22021": "character_not_in_repertoire", - "22008": "datetime_field_overflow", - "22012": "division_by_zero", - "22005": "error_in_assignment", - "2200B": "escape_character_conflict", - "22022": "indicator_overflow", - "22015": "interval_field_overflow", - "2201E": "invalid_argument_for_logarithm", - "22014": "invalid_argument_for_ntile_function", - "22016": "invalid_argument_for_nth_value_function", - "2201F": "invalid_argument_for_power_function", - "2201G": "invalid_argument_for_width_bucket_function", - "22018": "invalid_character_value_for_cast", - "22007": "invalid_datetime_format", - "22019": "invalid_escape_character", - "2200D": "invalid_escape_octet", - "22025": "invalid_escape_sequence", - "22P06": "nonstandard_use_of_escape_character", - "22010": "invalid_indicator_parameter_value", - "22023": "invalid_parameter_value", - "2201B": "invalid_regular_expression", - "2201W": "invalid_row_count_in_limit_clause", - "2201X": "invalid_row_count_in_result_offset_clause", - "22009": "invalid_time_zone_displacement_value", - "2200C": "invalid_use_of_escape_character", - "2200G": "most_specific_type_mismatch", - "22004": "null_value_not_allowed", - "22002": "null_value_no_indicator_parameter", - "22003": "numeric_value_out_of_range", - "2200H": "sequence_generator_limit_exceeded", - "22026": "string_data_length_mismatch", - "22001": "string_data_right_truncation", - "22011": "substring_error", - "22027": "trim_error", - "22024": "unterminated_c_string", - "2200F": "zero_length_character_string", - "22P01": "floating_point_exception", - "22P02": "invalid_text_representation", - "22P03": "invalid_binary_representation", - "22P04": "bad_copy_file_format", - "22P05": "untranslatable_character", - "2200L": "not_an_xml_document", - "2200M": "invalid_xml_document", - "2200N": "invalid_xml_content", - "2200S": "invalid_xml_comment", - "2200T": "invalid_xml_processing_instruction", - // Class 23 - Integrity Constraint Violation - "23000": "integrity_constraint_violation", - "23001": "restrict_violation", - "23502": "not_null_violation", - "23503": "foreign_key_violation", - "23505": "unique_violation", - "23514": "check_violation", - "23P01": "exclusion_violation", - // Class 24 - Invalid Cursor State - "24000": "invalid_cursor_state", - // Class 25 - Invalid Transaction State - "25000": "invalid_transaction_state", - "25001": "active_sql_transaction", - "25002": "branch_transaction_already_active", - "25008": "held_cursor_requires_same_isolation_level", - "25003": "inappropriate_access_mode_for_branch_transaction", - "25004": "inappropriate_isolation_level_for_branch_transaction", - "25005": "no_active_sql_transaction_for_branch_transaction", - "25006": "read_only_sql_transaction", - "25007": "schema_and_data_statement_mixing_not_supported", - "25P01": "no_active_sql_transaction", - "25P02": "in_failed_sql_transaction", - // Class 26 - Invalid SQL Statement Name - "26000": "invalid_sql_statement_name", - // Class 27 - Triggered Data Change Violation - "27000": "triggered_data_change_violation", - // Class 28 - Invalid Authorization Specification - "28000": "invalid_authorization_specification", - "28P01": "invalid_password", - // Class 2B - Dependent Privilege Descriptors Still Exist - "2B000": "dependent_privilege_descriptors_still_exist", - "2BP01": "dependent_objects_still_exist", - // Class 2D - Invalid Transaction Termination - "2D000": "invalid_transaction_termination", - // Class 2F - SQL Routine Exception - "2F000": "sql_routine_exception", - "2F005": "function_executed_no_return_statement", - "2F002": "modifying_sql_data_not_permitted", - "2F003": "prohibited_sql_statement_attempted", - "2F004": "reading_sql_data_not_permitted", - // Class 34 - Invalid Cursor Name - "34000": "invalid_cursor_name", - // Class 38 - External Routine Exception - "38000": "external_routine_exception", - "38001": "containing_sql_not_permitted", - "38002": "modifying_sql_data_not_permitted", - "38003": "prohibited_sql_statement_attempted", - "38004": "reading_sql_data_not_permitted", - // Class 39 - External Routine Invocation Exception - "39000": "external_routine_invocation_exception", - "39001": "invalid_sqlstate_returned", - "39004": "null_value_not_allowed", - "39P01": "trigger_protocol_violated", - "39P02": "srf_protocol_violated", - // Class 3B - Savepoint Exception - "3B000": "savepoint_exception", - "3B001": "invalid_savepoint_specification", - // Class 3D - Invalid Catalog Name - "3D000": "invalid_catalog_name", - // Class 3F - Invalid Schema Name - "3F000": "invalid_schema_name", - // Class 40 - Transaction Rollback - "40000": "transaction_rollback", - "40002": "transaction_integrity_constraint_violation", - "40001": "serialization_failure", - "40003": "statement_completion_unknown", - "40P01": "deadlock_detected", - // Class 42 - Syntax Error or Access Rule Violation - "42000": "syntax_error_or_access_rule_violation", - "42601": "syntax_error", - "42501": "insufficient_privilege", - "42846": "cannot_coerce", - "42803": "grouping_error", - "42P20": "windowing_error", - "42P19": "invalid_recursion", - "42830": "invalid_foreign_key", - "42602": "invalid_name", - "42622": "name_too_long", - "42939": "reserved_name", - "42804": "datatype_mismatch", - "42P18": "indeterminate_datatype", - "42P21": "collation_mismatch", - "42P22": "indeterminate_collation", - "42809": "wrong_object_type", - "42703": "undefined_column", - "42883": "undefined_function", - "42P01": "undefined_table", - "42P02": "undefined_parameter", - "42704": "undefined_object", - "42701": "duplicate_column", - "42P03": "duplicate_cursor", - "42P04": "duplicate_database", - "42723": "duplicate_function", - "42P05": "duplicate_prepared_statement", - "42P06": "duplicate_schema", - "42P07": "duplicate_table", - "42712": "duplicate_alias", - "42710": "duplicate_object", - "42702": "ambiguous_column", - "42725": "ambiguous_function", - "42P08": "ambiguous_parameter", - "42P09": "ambiguous_alias", - "42P10": "invalid_column_reference", - "42611": "invalid_column_definition", - "42P11": "invalid_cursor_definition", - "42P12": "invalid_database_definition", - "42P13": "invalid_function_definition", - "42P14": "invalid_prepared_statement_definition", - "42P15": "invalid_schema_definition", - "42P16": "invalid_table_definition", - "42P17": "invalid_object_definition", - // Class 44 - WITH CHECK OPTION Violation - "44000": "with_check_option_violation", - // Class 53 - Insufficient Resources - "53000": "insufficient_resources", - "53100": "disk_full", - "53200": "out_of_memory", - "53300": "too_many_connections", - "53400": "configuration_limit_exceeded", - // Class 54 - Program Limit Exceeded - "54000": "program_limit_exceeded", - "54001": "statement_too_complex", - "54011": "too_many_columns", - "54023": "too_many_arguments", - // Class 55 - Object Not In Prerequisite State - "55000": "object_not_in_prerequisite_state", - "55006": "object_in_use", - "55P02": "cant_change_runtime_param", - "55P03": "lock_not_available", - // Class 57 - Operator Intervention - "57000": "operator_intervention", - "57014": "query_canceled", - "57P01": "admin_shutdown", - "57P02": "crash_shutdown", - "57P03": "cannot_connect_now", - "57P04": "database_dropped", - // Class 58 - System Error (errors external to PostgreSQL itself) - "58000": "system_error", - "58030": "io_error", - "58P01": "undefined_file", - "58P02": "duplicate_file", - // Class F0 - Configuration File Error - "F0000": "config_file_error", - "F0001": "lock_file_exists", - // Class HV - Foreign Data Wrapper Error (SQL/MED) - "HV000": "fdw_error", - "HV005": "fdw_column_name_not_found", - "HV002": "fdw_dynamic_parameter_value_needed", - "HV010": "fdw_function_sequence_error", - "HV021": "fdw_inconsistent_descriptor_information", - "HV024": "fdw_invalid_attribute_value", - "HV007": "fdw_invalid_column_name", - "HV008": "fdw_invalid_column_number", - "HV004": "fdw_invalid_data_type", - "HV006": "fdw_invalid_data_type_descriptors", - "HV091": "fdw_invalid_descriptor_field_identifier", - "HV00B": "fdw_invalid_handle", - "HV00C": "fdw_invalid_option_index", - "HV00D": "fdw_invalid_option_name", - "HV090": "fdw_invalid_string_length_or_buffer_length", - "HV00A": "fdw_invalid_string_format", - "HV009": "fdw_invalid_use_of_null_pointer", - "HV014": "fdw_too_many_handles", - "HV001": "fdw_out_of_memory", - "HV00P": "fdw_no_schemas", - "HV00J": "fdw_option_name_not_found", - "HV00K": "fdw_reply_handle", - "HV00Q": "fdw_schema_not_found", - "HV00R": "fdw_table_not_found", - "HV00L": "fdw_unable_to_create_execution", - "HV00M": "fdw_unable_to_create_reply", - "HV00N": "fdw_unable_to_establish_connection", - // Class P0 - PL/pgSQL Error - "P0000": "plpgsql_error", - "P0001": "raise_exception", - "P0002": "no_data_found", - "P0003": "too_many_rows", - // Class XX - Internal Error - "XX000": "internal_error", - "XX001": "data_corrupted", - "XX002": "index_corrupted", -} +type ( + // ErrorCode is a five-character error code. + // + // Deprecated: use pqerror.Code + // + //go:fix inline + ErrorCode = pqerror.Code + + // ErrorClass is only the class part of an error code. + // + // Deprecated: use pqerror.Class + // + //go:fix inline + ErrorClass = pqerror.Class +) func parseError(r *readBuf, q string) *Error { err := &Error{query: q} @@ -438,7 +132,7 @@ func parseError(r *readBuf, q string) *Error { case 'S': err.Severity = msg case 'C': - err.Code = ErrorCode(msg) + err.Code = pqerror.Code(msg) case 'M': err.Message = msg case 'D': @@ -475,14 +169,10 @@ func parseError(r *readBuf, q string) *Error { } // Fatal returns true if the Error Severity is fatal. -func (e *Error) Fatal() bool { - return e.Severity == Efatal -} +func (e *Error) Fatal() bool { return e.Severity == pqerror.SeverityFatal } // SQLState returns the SQLState of the error. -func (e *Error) SQLState() string { - return string(e.Code) -} +func (e *Error) SQLState() string { return string(e.Code) } func (e *Error) Error() string { msg := e.Message diff --git a/error_test.go b/error_test.go index c1fbb3ef8..95dde1adf 100644 --- a/error_test.go +++ b/error_test.go @@ -7,11 +7,13 @@ import ( "fmt" "net" "os" + "reflect" "strconv" "testing" "time" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) func TestErrorSQLState(t *testing.T) { @@ -150,45 +152,6 @@ func TestError(t *testing.T) { } } -func BenchmarkError(b *testing.B) { - db := pqtest.MustDB(b) - _, err := db.Exec(pqtest.NormalizeIndent(` - create table browsers ( - browser_id serial, - name varchar, - version varchar - ); - create unique index "browsers#name#version" on browsers(name, version); - - create table systems ( - system_id serial, - name varchar, - version varchar, - ); - create unique index "systems#name#version" on systems(name, version); - `)) - if err == nil { - b.Fatal("err is nil?") - } - - b.ResetTimer() - b.Run("error", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = err.Error() - } - }) - b.Run("errorWithDetail", func(b *testing.B) { - pqErr := new(Error) - if !errors.As(err, &pqErr) { - b.Fatalf("not pq.Error: %T", err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = pqErr.ErrorWithDetail() - } - }) -} - type ( failConn struct{ net.Conn } failDialer struct{ d net.Dialer } @@ -298,3 +261,95 @@ func TestNetworkError(t *testing.T) { t.Fatal(err) } } + +func TestAs(t *testing.T) { + tests := []struct { + err error + codes []pqerror.Code + wantReturn bool + }{ + {nil, nil, false}, + {nil, []pqerror.Code{pqerror.SyntaxError}, false}, + {errors.New("oh noes"), []pqerror.Code{pqerror.SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, nil, true}, + + {&Error{Code: "00000", Message: "okay"}, []pqerror.Code{pqerror.SyntaxError}, false}, + {&Error{Code: "00000", Message: "okay"}, []pqerror.Code{pqerror.SyntaxError, pqerror.SuccessfulCompletion}, true}, + } + + t.Parallel() + for _, tt := range tests { + t.Run("", func(t *testing.T) { + have := As(tt.err, tt.codes...) + if tt.wantReturn { + if !reflect.DeepEqual(have, tt.err) { + t.Errorf("\nhave: %#v\nwant: %#v", have, tt.err) + } + } else { + if have != nil { + t.Errorf("expected return to be nil, but have:\n%#v", have) + } + } + }) + } +} + +func BenchmarkError(b *testing.B) { + db := pqtest.MustDB(b) + _, err := db.Exec(pqtest.NormalizeIndent(` + create table browsers ( + browser_id serial, + name varchar, + version varchar + ); + create unique index "browsers#name#version" on browsers(name, version); + + create table systems ( + system_id serial, + name varchar, + version varchar, + ); + create unique index "systems#name#version" on systems(name, version); + `)) + if err == nil { + b.Fatal("err is nil?") + } + + b.ResetTimer() + b.Run("error", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = err.Error() + } + }) + b.Run("errorWithDetail", func(b *testing.B) { + pqErr := new(Error) + if !errors.As(err, &pqErr) { + b.Fatalf("not pq.Error: %T", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pqErr.ErrorWithDetail() + } + }) +} + +func BenchmarkAs(b *testing.B) { + b.Run("nil", func(b *testing.B) { + var nilerr error + for i := 0; i < b.N; i++ { + _ = As(nilerr, pqerror.SuccessfulCompletion) + } + }) + b.Run("other error", func(b *testing.B) { + patherr := &os.PathError{} + for i := 0; i < b.N; i++ { + _ = As(patherr, pqerror.SuccessfulCompletion) + } + }) + b.Run("pq.Error", func(b *testing.B) { + pqerr := &Error{Code: "00000", Message: "okay"} + for i := 0; i < b.N; i++ { + _ = As(pqerr, pqerror.SuccessfulCompletion) + } + }) +} diff --git a/example_test.go b/example_test.go index 19511feac..0cc98011b 100644 --- a/example_test.go +++ b/example_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/lib/pq" + "github.com/lib/pq/pqerror" ) func Example_open() { @@ -339,3 +340,20 @@ func ExampleListener() { // notification on "banana" with data "yellow and curvy" // nil notify: closing Listener } + +func ExampleAs() { + db, err := sql.Open("postgres", "") + if err != nil { + log.Fatal(err) + } + + email := "hello@example.com" + + _, err = db.Exec("insert into t (email) values ($1)", email) + if pqErr := pq.As(err, pqerror.UniqueViolation); pqErr != nil { + log.Fatalf("email %q already exsts", email) + } + if err != nil { + log.Fatalf("unknown error: %s", err) + } +} diff --git a/helper_test.go b/helper_test.go index e5733a81b..902222af8 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1,30 +1,29 @@ package pq import ( + "slices" "testing" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) // Called for the side-effect of setting the environment. func init() { pqtest.DSN("") } -const cancelErrorCode ErrorCode = "57014" - -// pqError converts an error to *pq.Error, calling t.Fatal() if the error is nil -// or if this fails. +// mustAs calls As(), calling t.Fatal() if the error is nil or if this fails. // // This should probably be in pqtest, but can't right now due to import cycles, // and using pq_test package requires some refactoring as it refers to // unexported symbols. -func pqError(t *testing.T, err error) *Error { +func mustAs(t *testing.T, err error, codes ...pqerror.Code) *Error { t.Helper() - if err == nil { - t.Fatalf("pqError: error is nil") + pqErr := As(err) + if pqErr == nil { + t.Fatalf("mustAs: not *pq.Error: %T", err) } - pqErr, ok := err.(*Error) - if !ok { - t.Fatalf("wrong error %T: %[1]s", err) + if len(codes) > 0 && !slices.Contains(codes, pqErr.Code) { + t.Fatalf("mustAs: wrong error %q (code not one of %s)", pqErr.Error(), codes) } return pqErr } diff --git a/issues_test.go b/issues_test.go index 9816531a9..08e6022c4 100644 --- a/issues_test.go +++ b/issues_test.go @@ -3,11 +3,11 @@ package pq import ( "context" "database/sql" - "errors" "testing" "time" "github.com/lib/pq/internal/pqtest" + "github.com/lib/pq/pqerror" ) // #1046: stmt.QueryRowContext doesn't respect canceled context @@ -32,11 +32,7 @@ func TestQueryRowContext(t *testing.T) { t.Logf("FAIL %s: query returned after context deadline: %v\n", t.Name(), since) t.Fail() } - if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Logf("ctx.Err(): [%T]%+v\n", ctx.Err(), ctx.Err()) - t.Logf("got err: [%T] %+v expected errCode: %v", err, err, cancelErrorCode) - t.Fail() - } + mustAs(t, err, pqerror.QueryCanceled) } // #1062: drivers.ErrBadConn returned for DB.QueryRowContext.Scan when context is cancelled @@ -55,10 +51,10 @@ func TestQueryRowContextBad(t *testing.T) { var v int err := row.Scan(&v) - if pgErr := (*Error)(nil); err != nil && - err != context.Canceled && - !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("Scan resulted in unexpected error %v for canceled QueryRowContext at attempt %d", err, i+1) + + // nil, context Canceled, and QueryCancelled are all fine. + if err != nil && err != context.Canceled { + mustAs(t, err, pqerror.QueryCanceled) } } } @@ -97,9 +93,8 @@ func TestQueryCancelRace(t *testing.T) { row := db.QueryRowContext(ctx, "select pg_sleep(0.5)") var pgSleepVoid string err := row.Scan(&pgSleepVoid) - if pgErr := (*Error)(nil); !(errors.As(err, &pgErr) && pgErr.Code == cancelErrorCode) { - t.Fatalf("expected cancelled error; err=%#v", err) - } + + mustAs(t, err, pqerror.QueryCanceled) // get a connection: it must be a valid connIsValid(t, db) diff --git a/pqerror/codes.go b/pqerror/codes.go new file mode 100644 index 000000000..f55766440 --- /dev/null +++ b/pqerror/codes.go @@ -0,0 +1,581 @@ +// Code generated by gen.go. DO NOT EDIT. + +// Last updated for PostgreSQL 18.3 + +package pqerror + +var ( + ClassSuccessfulCompletion = Class("00") // Successful Completion + ClassWarning = Class("01") // Warning + ClassNoData = Class("02") // No Data (this is also a warning class per the SQL standard) + ClassSQLStatementNotYetComplete = Class("03") // SQL Statement Not Yet Complete + ClassConnectionException = Class("08") // Connection Exception + ClassTriggeredActionException = Class("09") // Triggered Action Exception + ClassFeatureNotSupported = Class("0A") // Feature Not Supported + ClassInvalidTransactionInitiation = Class("0B") // Invalid Transaction Initiation + ClassLocatorException = Class("0F") // Locator Exception + ClassInvalidGrantor = Class("0L") // Invalid Grantor + ClassInvalidRoleSpecification = Class("0P") // Invalid Role Specification + ClassDiagnosticsException = Class("0Z") // Diagnostics Exception + ClassCaseNotFound = Class("20") // Case Not Found + ClassCardinalityViolation = Class("21") // Cardinality Violation + ClassDataException = Class("22") // Data Exception + ClassIntegrityConstraintViolation = Class("23") // Integrity Constraint Violation + ClassInvalidCursorState = Class("24") // Invalid Cursor State + ClassInvalidTransactionState = Class("25") // Invalid Transaction State + ClassInvalidSQLStatementName = Class("26") // Invalid SQL Statement Name + ClassTriggeredDataChangeViolation = Class("27") // Triggered Data Change Violation + ClassInvalidAuthorizationSpecification = Class("28") // Invalid Authorization Specification + ClassDependentPrivilegeDescriptorsStillExist = Class("2B") // Dependent Privilege Descriptors Still Exist + ClassInvalidTransactionTermination = Class("2D") // Invalid Transaction Termination + ClassSQLRoutineException = Class("2F") // SQL Routine Exception + ClassInvalidCursorName = Class("34") // Invalid Cursor Name + ClassExternalRoutineException = Class("38") // External Routine Exception + ClassExternalRoutineInvocationException = Class("39") // External Routine Invocation Exception + ClassSavepointException = Class("3B") // Savepoint Exception + ClassInvalidCatalogName = Class("3D") // Invalid Catalog Name + ClassInvalidSchemaName = Class("3F") // Invalid Schema Name + ClassTransactionRollback = Class("40") // Transaction Rollback + ClassSyntaxErrorOrAccessRuleViolation = Class("42") // Syntax Error or Access Rule Violation + ClassWithCheckOptionViolation = Class("44") // WITH CHECK OPTION Violation + ClassInsufficientResources = Class("53") // Insufficient Resources + ClassProgramLimitExceeded = Class("54") // Program Limit Exceeded + ClassObjectNotInPrerequisiteState = Class("55") // Object Not In Prerequisite State + ClassOperatorIntervention = Class("57") // Operator Intervention + ClassSystemError = Class("58") // System Error (errors external to PostgreSQL itself) + ClassConfigFileError = Class("F0") // Configuration File Error + ClassFDWError = Class("HV") // Foreign Data Wrapper Error (SQL/MED) + ClassPLpgSQLError = Class("P0") // PL/pgSQL Error + ClassInternalError = Class("XX") // Internal Error +) + +// A list of all error codes used in PostgreSQL. +var ( + SuccessfulCompletion = Code("00000") // Class 00 - Successful Completion + Warning = Code("01000") // Class 01 - Warning + WarningDynamicResultSetsReturned = Code("0100C") + WarningImplicitZeroBitPadding = Code("01008") + WarningNullValueEliminatedInSetFunction = Code("01003") + WarningPrivilegeNotGranted = Code("01007") + WarningPrivilegeNotRevoked = Code("01006") + WarningStringDataRightTruncation = Code("01004") + WarningDeprecatedFeature = Code("01P01") + NoData = Code("02000") // Class 02 - No Data (this is also a warning class per the SQL standard) + NoAdditionalDynamicResultSetsReturned = Code("02001") + SQLStatementNotYetComplete = Code("03000") // Class 03 - SQL Statement Not Yet Complete + ConnectionException = Code("08000") // Class 08 - Connection Exception + ConnectionDoesNotExist = Code("08003") + ConnectionFailure = Code("08006") + SQLClientUnableToEstablishSQLConnection = Code("08001") + SQLServerRejectedEstablishmentOfSQLConnection = Code("08004") + TransactionResolutionUnknown = Code("08007") + ProtocolViolation = Code("08P01") + TriggeredActionException = Code("09000") // Class 09 - Triggered Action Exception + FeatureNotSupported = Code("0A000") // Class 0A - Feature Not Supported + InvalidTransactionInitiation = Code("0B000") // Class 0B - Invalid Transaction Initiation + LocatorException = Code("0F000") // Class 0F - Locator Exception + LEInvalidSpecification = Code("0F001") + InvalidGrantor = Code("0L000") // Class 0L - Invalid Grantor + InvalidGrantOperation = Code("0LP01") + InvalidRoleSpecification = Code("0P000") // Class 0P - Invalid Role Specification + DiagnosticsException = Code("0Z000") // Class 0Z - Diagnostics Exception + StackedDiagnosticsAccessedWithoutActiveHandler = Code("0Z002") + InvalidArgumentForXquery = Code("10608") + CaseNotFound = Code("20000") // Class 20 - Case Not Found + CardinalityViolation = Code("21000") // Class 21 - Cardinality Violation + DataException = Code("22000") // Class 22 - Data Exception + ArraySubscriptError = Code("2202E") + CharacterNotInRepertoire = Code("22021") + DatetimeFieldOverflow = Code("22008") + DivisionByZero = Code("22012") + ErrorInAssignment = Code("22005") + EscapeCharacterConflict = Code("2200B") + IndicatorOverflow = Code("22022") + IntervalFieldOverflow = Code("22015") + InvalidArgumentForLog = Code("2201E") + InvalidArgumentForNtile = Code("22014") + InvalidArgumentForNthValue = Code("22016") + InvalidArgumentForPowerFunction = Code("2201F") + InvalidArgumentForWidthBucketFunction = Code("2201G") + InvalidCharacterValueForCast = Code("22018") + InvalidDatetimeFormat = Code("22007") + InvalidEscapeCharacter = Code("22019") + InvalidEscapeOctet = Code("2200D") + InvalidEscapeSequence = Code("22025") + NonstandardUseOfEscapeCharacter = Code("22P06") + InvalidIndicatorParameterValue = Code("22010") + InvalidParameterValue = Code("22023") + InvalidPrecedingOrFollowingSize = Code("22013") + InvalidRegularExpression = Code("2201B") + InvalidRowCountInLimitClause = Code("2201W") + InvalidRowCountInResultOffsetClause = Code("2201X") + InvalidTablesampleArgument = Code("2202H") + InvalidTablesampleRepeat = Code("2202G") + InvalidTimeZoneDisplacementValue = Code("22009") + InvalidUseOfEscapeCharacter = Code("2200C") + MostSpecificTypeMismatch = Code("2200G") + NullValueNotAllowed = Code("22004") + NullValueNoIndicatorParameter = Code("22002") + NumericValueOutOfRange = Code("22003") + SequenceGeneratorLimitExceeded = Code("2200H") + StringDataLengthMismatch = Code("22026") + StringDataRightTruncation = Code("22001") + SubstringError = Code("22011") + TrimError = Code("22027") + UnterminatedCString = Code("22024") + ZeroLengthCharacterString = Code("2200F") + FloatingPointException = Code("22P01") + InvalidTextRepresentation = Code("22P02") + InvalidBinaryRepresentation = Code("22P03") + BadCopyFileFormat = Code("22P04") + UntranslatableCharacter = Code("22P05") + NotAnXMLDocument = Code("2200L") + InvalidXMLDocument = Code("2200M") + InvalidXMLContent = Code("2200N") + InvalidXMLComment = Code("2200S") + InvalidXMLProcessingInstruction = Code("2200T") + DuplicateJSONObjectKeyValue = Code("22030") + InvalidArgumentForSQLJSONDatetimeFunction = Code("22031") + InvalidJSONText = Code("22032") + InvalidSQLJSONSubscript = Code("22033") + MoreThanOneSQLJSONItem = Code("22034") + NoSQLJSONItem = Code("22035") + NonNumericSQLJSONItem = Code("22036") + NonUniqueKeysInAJSONObject = Code("22037") + SingletonSQLJSONItemRequired = Code("22038") + SQLJSONArrayNotFound = Code("22039") + SQLJSONMemberNotFound = Code("2203A") + SQLJSONNumberNotFound = Code("2203B") + SQLJSONObjectNotFound = Code("2203C") + TooManyJSONArrayElements = Code("2203D") + TooManyJSONObjectMembers = Code("2203E") + SQLJSONScalarRequired = Code("2203F") + SQLJSONItemCannotBeCastToTargetType = Code("2203G") + IntegrityConstraintViolation = Code("23000") // Class 23 - Integrity Constraint Violation + RestrictViolation = Code("23001") + NotNullViolation = Code("23502") + ForeignKeyViolation = Code("23503") + UniqueViolation = Code("23505") + CheckViolation = Code("23514") + ExclusionViolation = Code("23P01") + InvalidCursorState = Code("24000") // Class 24 - Invalid Cursor State + InvalidTransactionState = Code("25000") // Class 25 - Invalid Transaction State + ActiveSQLTransaction = Code("25001") + BranchTransactionAlreadyActive = Code("25002") + HeldCursorRequiresSameIsolationLevel = Code("25008") + InappropriateAccessModeForBranchTransaction = Code("25003") + InappropriateIsolationLevelForBranchTransaction = Code("25004") + NoActiveSQLTransactionForBranchTransaction = Code("25005") + ReadOnlySQLTransaction = Code("25006") + SchemaAndDataStatementMixingNotSupported = Code("25007") + NoActiveSQLTransaction = Code("25P01") + InFailedSQLTransaction = Code("25P02") + IdleInTransactionSessionTimeout = Code("25P03") + TransactionTimeout = Code("25P04") + InvalidSQLStatementName = Code("26000") // Class 26 - Invalid SQL Statement Name + TriggeredDataChangeViolation = Code("27000") // Class 27 - Triggered Data Change Violation + InvalidAuthorizationSpecification = Code("28000") // Class 28 - Invalid Authorization Specification + InvalidPassword = Code("28P01") + DependentPrivilegeDescriptorsStillExist = Code("2B000") // Class 2B - Dependent Privilege Descriptors Still Exist + DependentObjectsStillExist = Code("2BP01") + InvalidTransactionTermination = Code("2D000") // Class 2D - Invalid Transaction Termination + SQLRoutineException = Code("2F000") // Class 2F - SQL Routine Exception + SREFunctionExecutedNoReturnStatement = Code("2F005") + SREModifyingSQLDataNotPermitted = Code("2F002") + SREProhibitedSQLStatementAttempted = Code("2F003") + SREReadingSQLDataNotPermitted = Code("2F004") + InvalidCursorName = Code("34000") // Class 34 - Invalid Cursor Name + ExternalRoutineException = Code("38000") // Class 38 - External Routine Exception + EREContainingSQLNotPermitted = Code("38001") + EREModifyingSQLDataNotPermitted = Code("38002") + EREProhibitedSQLStatementAttempted = Code("38003") + EREReadingSQLDataNotPermitted = Code("38004") + ExternalRoutineInvocationException = Code("39000") // Class 39 - External Routine Invocation Exception + ERIEInvalidSQLSTATEReturned = Code("39001") + ERIENullValueNotAllowed = Code("39004") + ERIETriggerProtocolViolated = Code("39P01") + ERIESrfProtocolViolated = Code("39P02") + ERIEEventTriggerProtocolViolated = Code("39P03") + SavepointException = Code("3B000") // Class 3B - Savepoint Exception + SEInvalidSpecification = Code("3B001") + InvalidCatalogName = Code("3D000") // Class 3D - Invalid Catalog Name + InvalidSchemaName = Code("3F000") // Class 3F - Invalid Schema Name + TransactionRollback = Code("40000") // Class 40 - Transaction Rollback + TRIntegrityConstraintViolation = Code("40002") + TRSerializationFailure = Code("40001") + TRStatementCompletionUnknown = Code("40003") + TRDeadlockDetected = Code("40P01") + SyntaxErrorOrAccessRuleViolation = Code("42000") // Class 42 - Syntax Error or Access Rule Violation + SyntaxError = Code("42601") + InsufficientPrivilege = Code("42501") + CannotCoerce = Code("42846") + GroupingError = Code("42803") + WindowingError = Code("42P20") + InvalidRecursion = Code("42P19") + InvalidForeignKey = Code("42830") + InvalidName = Code("42602") + NameTooLong = Code("42622") + ReservedName = Code("42939") + DatatypeMismatch = Code("42804") + IndeterminateDatatype = Code("42P18") + CollationMismatch = Code("42P21") + IndeterminateCollation = Code("42P22") + WrongObjectType = Code("42809") + GeneratedAlways = Code("428C9") + UndefinedColumn = Code("42703") + UndefinedFunction = Code("42883") + UndefinedTable = Code("42P01") + UndefinedParameter = Code("42P02") + UndefinedObject = Code("42704") + DuplicateColumn = Code("42701") + DuplicateCursor = Code("42P03") + DuplicateDatabase = Code("42P04") + DuplicateFunction = Code("42723") + DuplicatePstatement = Code("42P05") + DuplicateSchema = Code("42P06") + DuplicateTable = Code("42P07") + DuplicateAlias = Code("42712") + DuplicateObject = Code("42710") + AmbiguousColumn = Code("42702") + AmbiguousFunction = Code("42725") + AmbiguousParameter = Code("42P08") + AmbiguousAlias = Code("42P09") + InvalidColumnReference = Code("42P10") + InvalidColumnDefinition = Code("42611") + InvalidCursorDefinition = Code("42P11") + InvalidDatabaseDefinition = Code("42P12") + InvalidFunctionDefinition = Code("42P13") + InvalidPstatementDefinition = Code("42P14") + InvalidSchemaDefinition = Code("42P15") + InvalidTableDefinition = Code("42P16") + InvalidObjectDefinition = Code("42P17") + WithCheckOptionViolation = Code("44000") // Class 44 - WITH CHECK OPTION Violation + InsufficientResources = Code("53000") // Class 53 - Insufficient Resources + DiskFull = Code("53100") + OutOfMemory = Code("53200") + TooManyConnections = Code("53300") + ConfigurationLimitExceeded = Code("53400") + ProgramLimitExceeded = Code("54000") // Class 54 - Program Limit Exceeded + StatementTooComplex = Code("54001") + TooManyColumns = Code("54011") + TooManyArguments = Code("54023") + ObjectNotInPrerequisiteState = Code("55000") // Class 55 - Object Not In Prerequisite State + ObjectInUse = Code("55006") + CantChangeRuntimeParam = Code("55P02") + LockNotAvailable = Code("55P03") + UnsafeNewEnumValueUsage = Code("55P04") + OperatorIntervention = Code("57000") // Class 57 - Operator Intervention + QueryCanceled = Code("57014") + AdminShutdown = Code("57P01") + CrashShutdown = Code("57P02") + CannotConnectNow = Code("57P03") + DatabaseDropped = Code("57P04") + IdleSessionTimeout = Code("57P05") + SystemError = Code("58000") // Class 58 - System Error (errors external to PostgreSQL itself) + IOError = Code("58030") + UndefinedFile = Code("58P01") + DuplicateFile = Code("58P02") + FileNameTooLong = Code("58P03") + ConfigFileError = Code("F0000") // Class F0 - Configuration File Error + LockFileExists = Code("F0001") + FDWError = Code("HV000") // Class HV - Foreign Data Wrapper Error (SQL/MED) + FDWColumnNameNotFound = Code("HV005") + FDWDynamicParameterValueNeeded = Code("HV002") + FDWFunctionSequenceError = Code("HV010") + FDWInconsistentDescriptorInformation = Code("HV021") + FDWInvalidAttributeValue = Code("HV024") + FDWInvalidColumnName = Code("HV007") + FDWInvalidColumnNumber = Code("HV008") + FDWInvalidDataType = Code("HV004") + FDWInvalidDataTypeDescriptors = Code("HV006") + FDWInvalidDescriptorFieldIdentifier = Code("HV091") + FDWInvalidHandle = Code("HV00B") + FDWInvalidOptionIndex = Code("HV00C") + FDWInvalidOptionName = Code("HV00D") + FDWInvalidStringLengthOrBufferLength = Code("HV090") + FDWInvalidStringFormat = Code("HV00A") + FDWInvalidUseOfNullPointer = Code("HV009") + FDWTooManyHandles = Code("HV014") + FDWOutOfMemory = Code("HV001") + FDWNoSchemas = Code("HV00P") + FDWOptionNameNotFound = Code("HV00J") + FDWReplyHandle = Code("HV00K") + FDWSchemaNotFound = Code("HV00Q") + FDWTableNotFound = Code("HV00R") + FDWUnableToCreateExecution = Code("HV00L") + FDWUnableToCreateReply = Code("HV00M") + FDWUnableToEstablishConnection = Code("HV00N") + PLpgSQLError = Code("P0000") // Class P0 - PL/pgSQL Error + RaiseException = Code("P0001") + NoDataFound = Code("P0002") + TooManyRows = Code("P0003") + AssertFailure = Code("P0004") + InternalError = Code("XX000") // Class XX - Internal Error + DataCorrupted = Code("XX001") + IndexCorrupted = Code("XX002") +) + +var errorCodeNames = map[Code]string{ + "00000": "successful_completion", + "01000": "warning", + "0100C": "dynamic_result_sets_returned", + "01008": "implicit_zero_bit_padding", + "01003": "null_value_eliminated_in_set_function", + "01007": "privilege_not_granted", + "01006": "privilege_not_revoked", + "01004": "string_data_right_truncation", + "01P01": "deprecated_feature", + "02000": "no_data", + "02001": "no_additional_dynamic_result_sets_returned", + "03000": "sql_statement_not_yet_complete", + "08000": "connection_exception", + "08003": "connection_does_not_exist", + "08006": "connection_failure", + "08001": "sqlclient_unable_to_establish_sqlconnection", + "08004": "sqlserver_rejected_establishment_of_sqlconnection", + "08007": "transaction_resolution_unknown", + "08P01": "protocol_violation", + "09000": "triggered_action_exception", + "0A000": "feature_not_supported", + "0B000": "invalid_transaction_initiation", + "0F000": "locator_exception", + "0F001": "invalid_locator_specification", + "0L000": "invalid_grantor", + "0LP01": "invalid_grant_operation", + "0P000": "invalid_role_specification", + "0Z000": "diagnostics_exception", + "0Z002": "stacked_diagnostics_accessed_without_active_handler", + "10608": "invalid_argument_for_xquery", + "20000": "case_not_found", + "21000": "cardinality_violation", + "22000": "data_exception", + "2202E": "array_subscript_error", + "22021": "character_not_in_repertoire", + "22008": "datetime_field_overflow", + "22012": "division_by_zero", + "22005": "error_in_assignment", + "2200B": "escape_character_conflict", + "22022": "indicator_overflow", + "22015": "interval_field_overflow", + "2201E": "invalid_argument_for_logarithm", + "22014": "invalid_argument_for_ntile_function", + "22016": "invalid_argument_for_nth_value_function", + "2201F": "invalid_argument_for_power_function", + "2201G": "invalid_argument_for_width_bucket_function", + "22018": "invalid_character_value_for_cast", + "22007": "invalid_datetime_format", + "22019": "invalid_escape_character", + "2200D": "invalid_escape_octet", + "22025": "invalid_escape_sequence", + "22P06": "nonstandard_use_of_escape_character", + "22010": "invalid_indicator_parameter_value", + "22023": "invalid_parameter_value", + "22013": "invalid_preceding_or_following_size", + "2201B": "invalid_regular_expression", + "2201W": "invalid_row_count_in_limit_clause", + "2201X": "invalid_row_count_in_result_offset_clause", + "2202H": "invalid_tablesample_argument", + "2202G": "invalid_tablesample_repeat", + "22009": "invalid_time_zone_displacement_value", + "2200C": "invalid_use_of_escape_character", + "2200G": "most_specific_type_mismatch", + "22004": "null_value_not_allowed", + "22002": "null_value_no_indicator_parameter", + "22003": "numeric_value_out_of_range", + "2200H": "sequence_generator_limit_exceeded", + "22026": "string_data_length_mismatch", + "22001": "string_data_right_truncation", + "22011": "substring_error", + "22027": "trim_error", + "22024": "unterminated_c_string", + "2200F": "zero_length_character_string", + "22P01": "floating_point_exception", + "22P02": "invalid_text_representation", + "22P03": "invalid_binary_representation", + "22P04": "bad_copy_file_format", + "22P05": "untranslatable_character", + "2200L": "not_an_xml_document", + "2200M": "invalid_xml_document", + "2200N": "invalid_xml_content", + "2200S": "invalid_xml_comment", + "2200T": "invalid_xml_processing_instruction", + "22030": "duplicate_json_object_key_value", + "22031": "invalid_argument_for_sql_json_datetime_function", + "22032": "invalid_json_text", + "22033": "invalid_sql_json_subscript", + "22034": "more_than_one_sql_json_item", + "22035": "no_sql_json_item", + "22036": "non_numeric_sql_json_item", + "22037": "non_unique_keys_in_a_json_object", + "22038": "singleton_sql_json_item_required", + "22039": "sql_json_array_not_found", + "2203A": "sql_json_member_not_found", + "2203B": "sql_json_number_not_found", + "2203C": "sql_json_object_not_found", + "2203D": "too_many_json_array_elements", + "2203E": "too_many_json_object_members", + "2203F": "sql_json_scalar_required", + "2203G": "sql_json_item_cannot_be_cast_to_target_type", + "23000": "integrity_constraint_violation", + "23001": "restrict_violation", + "23502": "not_null_violation", + "23503": "foreign_key_violation", + "23505": "unique_violation", + "23514": "check_violation", + "23P01": "exclusion_violation", + "24000": "invalid_cursor_state", + "25000": "invalid_transaction_state", + "25001": "active_sql_transaction", + "25002": "branch_transaction_already_active", + "25008": "held_cursor_requires_same_isolation_level", + "25003": "inappropriate_access_mode_for_branch_transaction", + "25004": "inappropriate_isolation_level_for_branch_transaction", + "25005": "no_active_sql_transaction_for_branch_transaction", + "25006": "read_only_sql_transaction", + "25007": "schema_and_data_statement_mixing_not_supported", + "25P01": "no_active_sql_transaction", + "25P02": "in_failed_sql_transaction", + "25P03": "idle_in_transaction_session_timeout", + "25P04": "transaction_timeout", + "26000": "invalid_sql_statement_name", + "27000": "triggered_data_change_violation", + "28000": "invalid_authorization_specification", + "28P01": "invalid_password", + "2B000": "dependent_privilege_descriptors_still_exist", + "2BP01": "dependent_objects_still_exist", + "2D000": "invalid_transaction_termination", + "2F000": "sql_routine_exception", + "2F005": "function_executed_no_return_statement", + "2F002": "modifying_sql_data_not_permitted", + "2F003": "prohibited_sql_statement_attempted", + "2F004": "reading_sql_data_not_permitted", + "34000": "invalid_cursor_name", + "38000": "external_routine_exception", + "38001": "containing_sql_not_permitted", + "38002": "modifying_sql_data_not_permitted", + "38003": "prohibited_sql_statement_attempted", + "38004": "reading_sql_data_not_permitted", + "39000": "external_routine_invocation_exception", + "39001": "invalid_sqlstate_returned", + "39004": "null_value_not_allowed", + "39P01": "trigger_protocol_violated", + "39P02": "srf_protocol_violated", + "39P03": "event_trigger_protocol_violated", + "3B000": "savepoint_exception", + "3B001": "invalid_savepoint_specification", + "3D000": "invalid_catalog_name", + "3F000": "invalid_schema_name", + "40000": "transaction_rollback", + "40002": "transaction_integrity_constraint_violation", + "40001": "serialization_failure", + "40003": "statement_completion_unknown", + "40P01": "deadlock_detected", + "42000": "syntax_error_or_access_rule_violation", + "42601": "syntax_error", + "42501": "insufficient_privilege", + "42846": "cannot_coerce", + "42803": "grouping_error", + "42P20": "windowing_error", + "42P19": "invalid_recursion", + "42830": "invalid_foreign_key", + "42602": "invalid_name", + "42622": "name_too_long", + "42939": "reserved_name", + "42804": "datatype_mismatch", + "42P18": "indeterminate_datatype", + "42P21": "collation_mismatch", + "42P22": "indeterminate_collation", + "42809": "wrong_object_type", + "428C9": "generated_always", + "42703": "undefined_column", + "42883": "undefined_function", + "42P01": "undefined_table", + "42P02": "undefined_parameter", + "42704": "undefined_object", + "42701": "duplicate_column", + "42P03": "duplicate_cursor", + "42P04": "duplicate_database", + "42723": "duplicate_function", + "42P05": "duplicate_prepared_statement", + "42P06": "duplicate_schema", + "42P07": "duplicate_table", + "42712": "duplicate_alias", + "42710": "duplicate_object", + "42702": "ambiguous_column", + "42725": "ambiguous_function", + "42P08": "ambiguous_parameter", + "42P09": "ambiguous_alias", + "42P10": "invalid_column_reference", + "42611": "invalid_column_definition", + "42P11": "invalid_cursor_definition", + "42P12": "invalid_database_definition", + "42P13": "invalid_function_definition", + "42P14": "invalid_prepared_statement_definition", + "42P15": "invalid_schema_definition", + "42P16": "invalid_table_definition", + "42P17": "invalid_object_definition", + "44000": "with_check_option_violation", + "53000": "insufficient_resources", + "53100": "disk_full", + "53200": "out_of_memory", + "53300": "too_many_connections", + "53400": "configuration_limit_exceeded", + "54000": "program_limit_exceeded", + "54001": "statement_too_complex", + "54011": "too_many_columns", + "54023": "too_many_arguments", + "55000": "object_not_in_prerequisite_state", + "55006": "object_in_use", + "55P02": "cant_change_runtime_param", + "55P03": "lock_not_available", + "55P04": "unsafe_new_enum_value_usage", + "57000": "operator_intervention", + "57014": "query_canceled", + "57P01": "admin_shutdown", + "57P02": "crash_shutdown", + "57P03": "cannot_connect_now", + "57P04": "database_dropped", + "57P05": "idle_session_timeout", + "58000": "system_error", + "58030": "io_error", + "58P01": "undefined_file", + "58P02": "duplicate_file", + "58P03": "file_name_too_long", + "F0000": "config_file_error", + "F0001": "lock_file_exists", + "HV000": "fdw_error", + "HV005": "fdw_column_name_not_found", + "HV002": "fdw_dynamic_parameter_value_needed", + "HV010": "fdw_function_sequence_error", + "HV021": "fdw_inconsistent_descriptor_information", + "HV024": "fdw_invalid_attribute_value", + "HV007": "fdw_invalid_column_name", + "HV008": "fdw_invalid_column_number", + "HV004": "fdw_invalid_data_type", + "HV006": "fdw_invalid_data_type_descriptors", + "HV091": "fdw_invalid_descriptor_field_identifier", + "HV00B": "fdw_invalid_handle", + "HV00C": "fdw_invalid_option_index", + "HV00D": "fdw_invalid_option_name", + "HV090": "fdw_invalid_string_length_or_buffer_length", + "HV00A": "fdw_invalid_string_format", + "HV009": "fdw_invalid_use_of_null_pointer", + "HV014": "fdw_too_many_handles", + "HV001": "fdw_out_of_memory", + "HV00P": "fdw_no_schemas", + "HV00J": "fdw_option_name_not_found", + "HV00K": "fdw_reply_handle", + "HV00Q": "fdw_schema_not_found", + "HV00R": "fdw_table_not_found", + "HV00L": "fdw_unable_to_create_execution", + "HV00M": "fdw_unable_to_create_reply", + "HV00N": "fdw_unable_to_establish_connection", + "P0000": "plpgsql_error", + "P0001": "raise_exception", + "P0002": "no_data_found", + "P0003": "too_many_rows", + "P0004": "assert_failure", + "XX000": "internal_error", + "XX001": "data_corrupted", + "XX002": "index_corrupted", +} diff --git a/pqerror/gen.go b/pqerror/gen.go new file mode 100644 index 000000000..e3b225167 --- /dev/null +++ b/pqerror/gen.go @@ -0,0 +1,146 @@ +//go:build ignore + +package main + +import ( + "bufio" + "bytes" + "fmt" + "go/format" + "os" + "regexp" + "strings" +) + +func version(src string) string { + fp, err := os.Open(src + "/meson.build") + if err != nil { + panic(err) + } + defer fp.Close() + + var ( + // egrep "^\s+version: ['\"][0-9.]+['\"],?$" meson.build | egrep -o '[0-9.]+' + reLine = regexp.MustCompile(`^\s+version: ['"][0-9.]+['"],?$`) + reNum = regexp.MustCompile(`[0-9.]+`) + scan = bufio.NewScanner(fp) + ) + for scan.Scan() { + line := scan.Text() + if reLine.MatchString(line) { + return reNum.FindString(line) + } + } + if scan.Err() != nil { + panic(scan.Err()) + } + panic("version not found in meson.build") +} + +func main() { + // TODO: don't hard-code this. Or fetch from git or something. + src := os.ExpandEnv("$HOME/src/postgresql") + + v := version(src) + + fp, err := os.Open(src + "/src/backend/utils/errcodes.txt") + if err != nil { + panic(err) + } + defer fp.Close() + + var ( + scan = bufio.NewScanner(fp) + re = regexp.MustCompile(`_[^_]+`) + out = new(bytes.Buffer) + codes = new(bytes.Buffer) + names = new(bytes.Buffer) + section string + ) + fmt.Fprintf(out, "// Code generated by gen.go. DO NOT EDIT.\n\n// Last updated for PostgreSQL %s\n\n", v) + out.WriteString("package pqerror\n\nvar (\n") + for scan.Scan() { + line := scan.Text() + if line == "" || line[0] == '#' { + continue + } + // Section: Class 01 - Warning + // Section: Class 08 - Connection Exception + if strings.HasPrefix(line, "Section: ") { + section = line[9:] + //fmt.Fprintf(names, "// %s\n", section) + continue + } + + // sqlstate E/W/S errcode_macro_name spec_name (optional) + // 01000 W ERRCODE_WARNING warning + // 22022 E ERRCODE_INDICATOR_OVERFLOW indicator_overflow + // 2202E E ERRCODE_ARRAY_ELEMENT_ERROR + f := strings.Fields(line) + if len(f) == 3 { + continue + } + if len(f) != 4 { + panic(fmt.Sprintf("invalid line: %q", line)) + } + sqlstate, typ, name, spec := f[0], f[1], f[2], f[3] + _ = typ // Not used atm + + name = strings.ToLower(name) + name = strings.Replace(name, "errcode_", "_", 1) + name = re.ReplaceAllStringFunc(name, func(m string) string { + switch m { + case "_xml", "_json", "_sql", "_fdw", "_io": + return strings.ToUpper(m[1:]) + case "_sqlclient": + return "SQLClient" + case "_sqlserver": + return "SQLServer" + case "_sqlconnection": + return "SQLConnection" + case "_sqlstate": + return "SQLSTATE" + case "_plpgsql": + return "PLpgSQL" + } + return string(m[1]^0x20) + m[2:] + }) + + class := strings.HasSuffix(sqlstate, "000") + + if class { + fmt.Fprintf(out, "\tClass%s = Class(%q) // %s\n", name, sqlstate[:2], section[11:]) + } + + fmt.Fprintf(names, "\t%q: %q,\n", sqlstate, spec) + + fmt.Fprintf(codes, "\t%s = Code(%q)", name, sqlstate) + if class { + fmt.Fprintf(codes, " // %s", section) + } + fmt.Fprintln(codes) + } + if scan.Err() != nil { + panic(scan.Err()) + } + out.WriteString(")\n") + + out.WriteString("// A list of all error codes used in PostgreSQL.\n") + out.WriteString("var (\n") + out.Write(codes.Bytes()) + out.WriteString(")\n\n") + + out.WriteString("var errorCodeNames = map[Code]string{\n") + out.Write(names.Bytes()) + out.WriteString("}\n") + + gosrc, err := format.Source(out.Bytes()) + if err != nil { + panic(err) + } + + err = os.WriteFile("codes.go", gosrc, 0o777) + if err != nil { + panic(err) + } +} diff --git a/pqerror/pqerror.go b/pqerror/pqerror.go new file mode 100644 index 000000000..29e49e999 --- /dev/null +++ b/pqerror/pqerror.go @@ -0,0 +1,35 @@ +//go:generate go run gen.go + +// Package pqerror contains PostgreSQL error codes for use with pq.Error. +package pqerror + +// Code is a five-character error code. +type Code string + +// Name returns a more human friendly rendering of the error code, namely the +// "condition name". +func (ec Code) Name() string { return errorCodeNames[ec] } + +// Class returns the error class, e.g. "28". +func (ec Code) Class() Class { return Class(ec[:2]) } + +// Class is only the class part of an error code. +type Class string + +// Name returns the condition name of an error class. It is equivalent to the +// condition name of the "standard" error code (i.e. the one having the last +// three characters "000"). +func (ec Class) Name() string { return errorCodeNames[Code(ec+"000")] } + +// TODO(v2): use "type Severity string" for the below. + +// Error severity values. +const ( + SeverityFatal = "FATAL" + SeverityPanic = "PANIC" + SeverityWarning = "WARNING" + SeverityNotice = "NOTICE" + SeverityDebug = "DEBUG" + SeverityInfo = "INFO" + SeverityLog = "LOG" +) diff --git a/ssl_test.go b/ssl_test.go index 0eb5f17cb..07e548392 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -27,7 +27,7 @@ func startSSLTest(t *testing.T, user string) { wantErr = "internal_error" } _, err := pqtest.DB(t, "sslmode=disable user="+user) - pqErr := pqError(t, err) + pqErr := mustAs(t, err) if pqErr.Code.Name() != wantErr { t.Fatalf("wrong error code %q", pqErr.Code.Name()) }