diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fd8f5859..a7c8cb7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ newer. Previously PostgreSQL 8.4 and newer were supported. - Support [`sslnegotiation`] to use SSL without negotiation ([#1180]). +- The `pq.Error.Error()` text includes the position of the error (if reported + by PostgreSQL) and SQLSTATE code ([#1224]): + + pq: column "columndoesntexist" does not exist at column 8 (42703) + pq: syntax error at or near ")" at position 2:71 (42601) + - The `pq.Error.ErrorWithDetail()` method prints a more detailed multiline message, with the Detail, Hint, and error position (if any) ([#1219]): @@ -81,6 +87,7 @@ newer. Previously PostgreSQL 8.4 and newer were supported. [#1214]: https://github.com/lib/pq/pull/1214 [#1219]: https://github.com/lib/pq/pull/1219 [#1223]: https://github.com/lib/pq/pull/1223 +[#1224]: https://github.com/lib/pq/pull/1224 v1.10.9 (2023-04-26) diff --git a/conn_test.go b/conn_test.go index a65893af8..67c3fc21d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1888,7 +1888,6 @@ func TestStmtQueryContext(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() db := pqtest.MustDB(t) defer db.Close() @@ -1950,7 +1949,6 @@ func TestStmtExecContext(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() db := pqtest.MustDB(t) defer db.Close() @@ -2435,7 +2433,6 @@ func TestAuth(t *testing.T) { for _, tt := range tests { t.Run(tt.conn, func(t *testing.T) { - t.Parallel() db, err := pqtest.DB(tt.conn) if err != nil { t.Fatal(err) diff --git a/error.go b/error.go index c361f1157..d190895fd 100644 --- a/error.go +++ b/error.go @@ -485,10 +485,24 @@ func (e *Error) SQLState() string { } func (e *Error) Error() string { + msg := e.Message + if e.query != "" && e.Position != "" { + pos, err := strconv.Atoi(e.Position) + if err == nil { + lines := strings.Split(e.query, "\n") + line, col := posToLine(pos, lines) + if len(lines) == 1 { + msg += " at column " + strconv.Itoa(col) + } else { + msg += " at position " + strconv.Itoa(line) + ":" + strconv.Itoa(col) + } + } + } + if e.Code != "" { - return "pq: " + e.Message + " (" + string(e.Code) + ")" + return "pq: " + msg + " (" + string(e.Code) + ")" } - return "pq: " + e.Message + return "pq: " + msg } // ErrorWithDetail returns the error message with detailed information and diff --git a/error_test.go b/error_test.go index e2ac8fb0c..51aae76fb 100644 --- a/error_test.go +++ b/error_test.go @@ -31,21 +31,21 @@ func TestError(t *testing.T) { ERROR: cannot copy from view "x" (42809) HINT: Try the COPY (SELECT ...) TO variant. `}, - {`select columndoesntexist`, `pq: column "columndoesntexist" does not exist (42703)`, ` + {`select columndoesntexist`, `pq: column "columndoesntexist" does not exist at column 8 (42703)`, ` ERROR: column "columndoesntexist" does not exist (42703) CONTEXT: line 1, column 8: 1 | select columndoesntexist ^ `}, - {`select !@#`, "pq: syntax error at end of input (42601)", ` + {`select !@#`, "pq: syntax error at end of input at column 11 (42601)", ` ERROR: syntax error at end of input (42601) CONTEXT: line 1, column 11: 1 | select !@# ^ `}, - {"select 'asd',\n\t'asd'::jsonb", "pq: invalid input syntax for type json (22P02)", ` + {"select 'asd',\n\t'asd'::jsonb", "pq: invalid input syntax for type json at position 2:2 (22P02)", ` ERROR: invalid input syntax for type json (22P02) DETAIL: Token "asd" is invalid. CONTEXT: line 2, column 2: @@ -54,7 +54,7 @@ func TestError(t *testing.T) { 2 | 'asd'::jsonb ^ `}, - {"select 'asd'\n,'zxc',\n'def',\n123,\n'foo', 'asd'::jsonb", "pq: invalid input syntax for type json (22P02)", ` + {"select 'asd'\n,'zxc',\n'def',\n123,\n'foo', 'asd'::jsonb", "pq: invalid input syntax for type json at position 5:8 (22P02)", ` ERROR: invalid input syntax for type json (22P02) DETAIL: Token "asd" is invalid. CONTEXT: line 5, column 8: @@ -64,14 +64,14 @@ func TestError(t *testing.T) { 5 | 'foo', 'asd'::jsonb ^ `}, - {"select '€€€', a", `pq: column "a" does not exist (42703)`, ` + {"select '€€€', a", `pq: column "a" does not exist at column 15 (42703)`, ` ERROR: column "a" does not exist (42703) CONTEXT: line 1, column 15: 1 | select '€€€', a ^ `}, - {"select '€€€',\n'€',a", `pq: column "a" does not exist (42703)`, ` + {"select '€€€',\n'€',a", `pq: column "a" does not exist at position 2:5 (42703)`, ` ERROR: column "a" does not exist (42703) CONTEXT: line 2, column 5: @@ -93,7 +93,7 @@ func TestError(t *testing.T) { version varchar, ); create unique index "systems#name#version" on systems(name, version); - `), `pq: syntax error at or near ")" (42601)`, ` + `), `pq: syntax error at or near ")" at position 12:1 (42601)`, ` ERROR: syntax error at or near ")" (42601) CONTEXT: line 12, column 1: @@ -106,7 +106,7 @@ func TestError(t *testing.T) { {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); - `), `pq: syntax error at or near ")" (42601)`, ` + `), `pq: syntax error at or near ")" at position 2:71 (42601)`, ` ERROR: syntax error at or near ")" (42601) CONTEXT: line 2, column 71: @@ -116,27 +116,68 @@ func TestError(t *testing.T) { `}, } + t.Parallel() db := pqtest.MustDB(t) - for _, tt := range tests { - _, err := db.Exec(tt.in) - if err == nil { - t.Fatal("no error?") + t.Run("", func(t *testing.T) { + _, err := db.Exec(tt.in) + if err == nil { + t.Fatal("no error?") + } + pqErr := new(Error) + if !errors.As(err, &pqErr) { + t.Fatalf("wrong error %T: %[1]s", err) + } + + if err.Error() != tt.want { + t.Errorf("\nhave: %s\nwant: %s", err.Error(), tt.want) + } + tt.wantDetail = pqtest.NormalizeIndent(tt.wantDetail) + if pqErr.query != "" && pqErr.Position != "" { + tt.wantDetail += "\n" + } + if pqErr.ErrorWithDetail() != tt.wantDetail { + t.Errorf("\nhave:\n%s\nwant:\n%s", pqErr.ErrorWithDetail(), tt.wantDetail) + } + }) + } +} + +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) { - t.Fatalf("wrong error %T: %[1]s", err) - } - - if err.Error() != tt.want { - t.Errorf("\nhave: %s\nwant: %s", err.Error(), tt.want) + b.Fatalf("not pq.Error: %T", err) } - tt.wantDetail = pqtest.NormalizeIndent(tt.wantDetail) - if pqErr.query != "" && pqErr.Position != "" { - tt.wantDetail += "\n" + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = pqErr.ErrorWithDetail() } - if pqErr.ErrorWithDetail() != tt.wantDetail { - t.Errorf("\nhave:\n%s\nwant:\n%s", pqErr.ErrorWithDetail(), tt.wantDetail) - } - } + }) }