Skip to content

Commit 4e508a9

Browse files
vietjjulianladisch
andauthored
PgException getter and getMessage with all fields, PgErrorCodes (#642)
* PgException getter and getMessage with all fields, PgErrorCodes Support debugging database errors by including all fields in PgException. Fixes #274. * Create a getter method for each field returned by PostgreSQL. * The getMessage() method (overriding the RuntimeException method) has been returning the message field only. Now it returns all fields for complete information in logs. It creates a serialized JsonObject to support parsing the String, for example in microservice integration tests where it is returned in the HTTP body. * To get only the message field use the new getErrorMessage() method. * Add PgErrorCodes with constants for all SQLSTATE error codes and a hasCode(Throwable throwable, String code) method for comparison. "Applications that need to know which error condition has occurred should usually test the error code, rather than looking at the textual error message." (Quote from https://www.postgresql.org/docs/current/errcodes-appendix.html ) The https://github.com/vert-x3/vertx-mysql-postgresql-client client returns all fields in GenericDatabaseException#getMessage(), PgException should provide the same amount of data. To change the visibility of the Response getter methods from package-private to public the package-private Response has been renamed to ResponseImpl and a public Response interface with public getter methods has been created. Signed-off-by: Julian Ladisch <eclipse.org-rtn@ladisch.de> * Copy Response fields into PgException, revert Response/ResponseImpl Signed-off-by: Julian Ladisch <eclipse.org-rtn@ladisch.de> * Add license header Signed-off-by: Julian Ladisch <eclipse.org-rtn@ladisch.de> Co-authored-by: Julian Ladisch <eclipse.org-rtn@ladisch.de>
2 parents 0a1a058 + 9a9d440 commit 4e508a9

File tree

7 files changed

+360
-7
lines changed

7 files changed

+360
-7
lines changed

vertx-pg-client/src/main/java/io/vertx/pgclient/PgException.java

Lines changed: 225 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,256 @@
1717

1818
package io.vertx.pgclient;
1919

20+
import io.vertx.core.json.Json;
21+
2022
/**
23+
* PostgreSQL error including all <a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">fields
24+
* of the ErrorResponse message</a> of the PostgreSQL frontend/backend protocol.
25+
*
2126
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
2227
*/
2328
public class PgException extends RuntimeException {
2429

30+
private final String errorMessage;
2531
private final String severity;
2632
private final String code;
2733
private final String detail;
34+
private final String hint;
35+
private final String position;
36+
private final String internalPosition;
37+
private final String internalQuery;
38+
private final String where;
39+
private final String file;
40+
private final String line;
41+
private final String routine;
42+
private final String schema;
43+
private final String table;
44+
private final String column;
45+
private final String dataType;
46+
private final String constraint;
47+
48+
public PgException(String errorMessage, String severity, String code, String detail) {
49+
this.errorMessage = errorMessage;
50+
this.severity = severity;
51+
this.code = code;
52+
this.detail = detail;
53+
this.hint = null;
54+
this.position = null;
55+
this.internalPosition = null;
56+
this.internalQuery = null;
57+
this.where = null;
58+
this.file = null;
59+
this.line = null;
60+
this.routine = null;
61+
this.schema = null;
62+
this.table = null;
63+
this.column = null;
64+
this.dataType = null;
65+
this.constraint = null;
66+
}
2867

29-
public PgException(String message, String severity, String code, String detail) {
30-
super(message);
68+
public PgException(String errorMessage, String severity, String code, String detail, String hint, String position,
69+
String internalPosition, String internalQuery, String where, String file, String line, String routine,
70+
String schema, String table, String column, String dataType, String constraint) {
71+
this.errorMessage = errorMessage;
3172
this.severity = severity;
3273
this.code = code;
3374
this.detail = detail;
75+
this.hint = hint;
76+
this.position = position;
77+
this.internalPosition = internalPosition;
78+
this.internalQuery = internalQuery;
79+
this.where = where;
80+
this.file = file;
81+
this.line = line;
82+
this.routine = routine;
83+
this.schema = schema;
84+
this.table = table;
85+
this.column = column;
86+
this.dataType = dataType;
87+
this.constraint = constraint;
3488
}
3589

90+
/**
91+
* @return the primary human-readable error message
92+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'M' field</a>)
93+
*/
94+
public String getErrorMessage() {
95+
// getErrorMessage() avoids name clash with RuntimeException#getMessage()
96+
return errorMessage;
97+
}
98+
99+
/**
100+
* @return the severity: ERROR, FATAL, or PANIC, or a localized translation of one of these
101+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'S' field</a>)
102+
*/
36103
public String getSeverity() {
37104
return severity;
38105
}
39106

107+
/**
108+
* @return the SQLSTATE code for the error
109+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'S' field</a>,
110+
* <a href="https://www.postgresql.org/docs/current/errcodes-appendix.html">value list</a>),
111+
* it is never localized
112+
*/
40113
public String getCode() {
41114
return code;
42115
}
43116

44117
/**
45-
* @return the detail error message
118+
* @return an optional secondary error message carrying more detail about the problem
119+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'D' field</a>),
120+
* a newline indicates paragraph break.
46121
*/
47122
public String getDetail() {
48123
return detail;
49124
}
125+
126+
/**
127+
* @return an optional suggestion (advice) what to do about the problem
128+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'H' field</a>),
129+
* a newline indicates paragraph break.
130+
*/
131+
public String getHint() {
132+
return hint;
133+
}
134+
135+
/**
136+
* @return a decimal ASCII integer, indicating an error cursor position as an index into the original
137+
* query string. (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'P' field</a>).
138+
* The first character has index 1, and positions are measured in characters not bytes.
139+
*/
140+
public String getPosition() {
141+
return position;
142+
}
143+
144+
/**
145+
* @return an indication of the context in which the error occurred
146+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'W' field</a>).
147+
* Presently this includes a call stack traceback of active procedural language functions and
148+
* internally-generated queries. The trace is one entry per line, most recent first.
149+
*/
150+
public String getWhere() {
151+
return where;
152+
}
153+
154+
/**
155+
* @return file name of the source-code location where the error was reported
156+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'F' field</a>).
157+
*/
158+
public String getFile() {
159+
return file;
160+
}
161+
162+
/**
163+
* @return line number of the source-code location where the error was reported
164+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'L' field</a>).
165+
*/
166+
public String getLine() {
167+
return line;
168+
}
169+
170+
/**
171+
* @return name of the source-code routine reporting the error
172+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'R' field</a>).
173+
*/
174+
public String getRoutine() {
175+
return routine;
176+
}
177+
178+
/**
179+
* @return if the error was associated with a specific database object, the name of the schema containing
180+
* that object, if any
181+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'s' field</a>).
182+
*/
183+
public String getSchema() {
184+
return schema;
185+
}
186+
187+
/**
188+
* @return if the error was associated with a specific table, the name of the table
189+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'t' field</a>).
190+
*/
191+
public String getTable() {
192+
return table;
193+
}
194+
195+
/**
196+
* @return if the error was associated with a specific table column, the name of the column
197+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'c' field</a>).
198+
*/
199+
public String getColumn() {
200+
return column;
201+
}
202+
203+
/**
204+
* @return if the error was associated with a specific data type, the name of the data type
205+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'d' field</a>).
206+
*/
207+
public String getDataType() {
208+
return dataType;
209+
}
210+
211+
/**
212+
* @return if the error was associated with a specific constraint, the name of the constraint
213+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'n' field</a>).
214+
*/
215+
public String getConstraint() {
216+
return constraint;
217+
}
218+
219+
/**
220+
* @return a decimal ASCII integer, indicating an error cursor position
221+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'p' field</a>)
222+
* as an index into the internally generated command (see 'q' field).
223+
*/
224+
public String getInternalPosition() {
225+
return internalPosition;
226+
}
227+
228+
/**
229+
* @return the text of a failed internally-generated command
230+
* (<a href="https://www.postgresql.org/docs/current/protocol-error-fields.html">'q' field</a>).
231+
*/
232+
public String getInternalQuery() {
233+
return internalQuery;
234+
}
235+
236+
private static void append(StringBuffer stringBuffer, String key, String value) {
237+
if (value != null) {
238+
stringBuffer.append(", \"").append(key).append("\": ").append(Json.encode(value));
239+
}
240+
}
241+
242+
/**
243+
* A serialized JsonObject of all non-null error message fields.
244+
*/
245+
@Override
246+
public String getMessage() {
247+
StringBuffer sb = new StringBuffer();
248+
append(sb, "message", getErrorMessage());
249+
append(sb, "severity", getSeverity());
250+
append(sb, "code", getCode());
251+
append(sb, "detail", getDetail());
252+
append(sb, "hint", getHint());
253+
append(sb, "position", getPosition());
254+
append(sb, "internalPosition", getInternalPosition());
255+
append(sb, "internalQuery", getInternalQuery());
256+
append(sb, "where", getWhere());
257+
append(sb, "file", getFile());
258+
append(sb, "line", getLine());
259+
append(sb, "routine", getRoutine());
260+
append(sb, "schema", getSchema());
261+
append(sb, "table", getTable());
262+
append(sb, "column", getColumn());
263+
append(sb, "dataType", getDataType());
264+
append(sb, "constraint", getConstraint());
265+
if (sb.length() == 0) {
266+
return "{}";
267+
}
268+
sb.append(" }");
269+
sb.setCharAt(0, '{'); // replace leading comma
270+
return sb.toString();
271+
}
50272
}

vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/ErrorResponse.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
class ErrorResponse extends Response {
2727

2828
PgException toException() {
29-
return new PgException(getMessage(), getSeverity(), getCode(), getDetail());
29+
return new PgException(getMessage(), getSeverity(), getCode(), getDetail(), getHint(),
30+
getPosition(), getInternalPosition(), getInternalQuery(), getWhere(), getFile(), getLine(), getRoutine(),
31+
getSchema(), getTable(), getColumn(), getDataType(), getConstraint());
3032
}
3133

3234
@Override

vertx-pg-client/src/test/java/io/vertx/pgclient/PgConnectionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public void testCancelRequest(TestContext ctx) {
106106
conn
107107
.query("SELECT pg_sleep(10)")
108108
.execute(ctx.asyncAssertFailure(error -> {
109-
ctx.assertEquals("canceling statement due to user request", error.getMessage());
109+
ctx.assertTrue(hasSqlstateCode(error, ERRCODE_QUERY_CANCELED), error.getMessage());
110110
async.countDown();
111111
}));
112112
((PgConnection)conn).cancelRequest(ctx.asyncAssertSuccess());
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) 2020 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
12+
package io.vertx.pgclient;
13+
14+
import org.junit.Test;
15+
16+
import io.vertx.core.json.JsonObject;
17+
import io.vertx.pgclient.impl.codec.ResponseHelper;
18+
19+
import static org.junit.Assert.*;
20+
21+
public class PgExceptionTest {
22+
23+
@Test
24+
public void fieldConstructor() {
25+
PgException pgException = new PgException("myMessage", "mySeverity", "myCode", "myDetail");
26+
assertEquals("myMessage", pgException.getErrorMessage());
27+
assertEquals("mySeverity", pgException.getSeverity());
28+
assertEquals("myCode", pgException.getCode());
29+
assertEquals("myDetail", pgException.getDetail());
30+
31+
JsonObject jsonObject = new JsonObject(pgException.getMessage());
32+
assertEquals("myMessage", jsonObject.getString("message"));
33+
assertEquals("mySeverity", jsonObject.getString("severity"));
34+
assertEquals("myCode", jsonObject.getString("code"));
35+
assertEquals("myDetail", jsonObject.getString("detail"));
36+
}
37+
38+
@Test
39+
public void nullFieldConstructor() {
40+
assertEquals("{}", new PgException(null, null, null, null).getMessage());
41+
}
42+
43+
@Test
44+
public void errorResponseConstructor() {
45+
PgException pgException = ResponseHelper.getCompletePgException();
46+
assertEquals("myMessage", pgException.getErrorMessage());
47+
assertEquals("mySeverity", pgException.getSeverity());
48+
assertEquals("myCode", pgException.getCode());
49+
assertEquals("myDetail", pgException.getDetail());
50+
51+
// getMessage() should return a valid JsonObject with all fields
52+
JsonObject jsonObject = new JsonObject(pgException.getMessage());
53+
assertEquals("myMessage", jsonObject.getString("message"));
54+
assertEquals("mySeverity", jsonObject.getString("severity"));
55+
assertEquals("myCode", jsonObject.getString("code"));
56+
assertEquals("myDetail", jsonObject.getString("detail"));
57+
assertEquals("myHint", jsonObject.getString("hint"));
58+
assertEquals("myPosition", jsonObject.getString("position"));
59+
assertEquals("myInternalPosition", jsonObject.getString("internalPosition"));
60+
assertEquals("myInternalQuery", jsonObject.getString("internalQuery"));
61+
assertEquals("myWhere", jsonObject.getString("where"));
62+
assertEquals("myFile", jsonObject.getString("file"));
63+
assertEquals("myLine", jsonObject.getString("line"));
64+
assertEquals("myRoutine", jsonObject.getString("routine"));
65+
assertEquals("mySchema", jsonObject.getString("schema"));
66+
assertEquals("myTable", jsonObject.getString("table"));
67+
assertEquals("myColumn", jsonObject.getString("column"));
68+
assertEquals("myDataType", jsonObject.getString("dataType"));
69+
assertEquals("myConstraint", jsonObject.getString("constraint"));
70+
}
71+
72+
@Test
73+
public void nullErrorResponseConstructor() {
74+
assertEquals("{}", ResponseHelper.getEmptyPgException().getMessage());
75+
}
76+
}

vertx-pg-client/src/test/java/io/vertx/pgclient/PgPoolTestBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public void testCancelRequest(TestContext ctx) {
174174
PgPool pool = createPool(options, 4);
175175
pool.getConnection(ctx.asyncAssertSuccess(conn -> {
176176
conn.query("SELECT pg_sleep(10)").execute(ctx.asyncAssertFailure(error -> {
177-
ctx.assertEquals("canceling statement due to user request", error.getMessage());
177+
ctx.assertTrue(hasSqlstateCode(error, ERRCODE_QUERY_CANCELED), error.getMessage());
178178
conn.close();
179179
async.complete();
180180
}));

vertx-pg-client/src/test/java/io/vertx/pgclient/PgTestBase.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
@RunWith(VertxUnitRunner.class)
3535
public abstract class PgTestBase {
3636

37+
protected static final String ERRCODE_QUERY_CANCELED = "57014";
38+
3739
@ClassRule
3840
public static ContainerPgRule rule = new ContainerPgRule();
3941

@@ -61,5 +63,11 @@ static void insertIntoTestTable(TestContext ctx, SqlClient client, int amount, R
6163
}
6264
}
6365

64-
66+
/**
67+
* @return whether throwable is a PgException with the SQLSTATE code
68+
*/
69+
static boolean hasSqlstateCode(Throwable throwable, String code) {
70+
return throwable instanceof PgException &&
71+
code.equals(((PgException) throwable).getCode());
72+
}
6573
}

0 commit comments

Comments
 (0)