Skip to content

Commit 5874f8b

Browse files
feat: include PostgreSQL error code in exceptions (#4236)
* feat: include PostgreSQL error code in exceptions PostgreSQL error codes are now included in SpannerException when available. The PostgreSQL error code is only filled with a non-null value if the database is a Spanner PostgreSQL-dialect database, and the error is one that has a corresponding error code in PostgreSQL. See https://www.postgresql.org/docs/current/errcodes-appendix.html for a full list of all PostgreSQL error codes. * chore: generate libraries at Wed Nov 19 17:51:21 UTC 2025 * chore: fix formatting and tests --------- Co-authored-by: cloud-java-bot <[email protected]>
1 parent 0834317 commit 5874f8b

File tree

13 files changed

+83
-47
lines changed

13 files changed

+83
-47
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private <R> R runTransaction(final AsyncWork<R> work) {
5959
try {
6060
return work.doWorkAsync(transaction).get();
6161
} catch (ExecutionException e) {
62-
throw SpannerExceptionFactory.newSpannerException(e.getCause());
62+
throw SpannerExceptionFactory.asSpannerException(e.getCause());
6363
} catch (InterruptedException e) {
6464
throw SpannerExceptionFactory.propagateInterrupt(e);
6565
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private ApiFuture<TransactionContext> internalBeginAsync(
123123
@Override
124124
public void onFailure(Throwable t) {
125125
onError(t);
126-
res.setException(SpannerExceptionFactory.newSpannerException(t));
126+
res.setException(SpannerExceptionFactory.asSpannerException(t));
127127
}
128128

129129
@Override

google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package com.google.cloud.spanner;
1818

19-
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
19+
import static com.google.cloud.spanner.SpannerExceptionFactory.asSpannerException;
2020
import static com.google.common.base.Preconditions.checkState;
2121

2222
import com.google.api.core.InternalApi;
@@ -76,7 +76,7 @@ protected GrpcStruct currRow() {
7676
@Override
7777
public boolean next() throws SpannerException {
7878
if (error != null) {
79-
throw newSpannerException(error);
79+
throw asSpannerException(error);
8080
}
8181
try {
8282
if (currRow == null) {
@@ -108,7 +108,7 @@ public boolean next() throws SpannerException {
108108
return hasNext;
109109
} catch (Throwable t) {
110110
throw yieldError(
111-
SpannerExceptionFactory.asSpannerException(t),
111+
asSpannerException(t),
112112
iterator.isWithBeginTransaction() && currRow == null,
113113
iterator.isLastStatement());
114114
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ protected final PartialResultSet computeNext() {
155155
call = null;
156156

157157
if (error != null) {
158-
throw SpannerExceptionFactory.newSpannerException(error);
158+
throw SpannerExceptionFactory.asSpannerException(error);
159159
}
160160

161161
endOfData();
@@ -192,25 +192,17 @@ public void onCompleted() {
192192
}
193193

194194
@Override
195-
public void onError(SpannerException e) {
195+
public void onError(SpannerException exception) {
196196
if (statement != null) {
197197
if (logger.isLoggable(Level.FINEST)) {
198198
// Include parameter values if logging level is set to FINEST or higher.
199-
e =
200-
SpannerExceptionFactory.newSpannerExceptionPreformatted(
201-
e.getErrorCode(),
202-
String.format("%s - Statement: '%s'", e.getMessage(), statement.toString()),
203-
e);
204-
logger.log(Level.FINEST, "Error executing statement", e);
199+
exception.setStatement(statement.toString());
200+
logger.log(Level.FINEST, "Error executing statement", exception);
205201
} else {
206-
e =
207-
SpannerExceptionFactory.newSpannerExceptionPreformatted(
208-
e.getErrorCode(),
209-
String.format("%s - Statement: '%s'", e.getMessage(), statement.getSql()),
210-
e);
202+
exception.setStatement(statement.getSql());
211203
}
212204
}
213-
error = e;
205+
error = exception;
214206
addToStream(END_OF_STREAM);
215207
}
216208

google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ private void verifyBeginTransactionWithRWOnMultiplexedSessionAsync(String sessio
498498
}
499499
readWriteBeginTransactionReferenceFuture.set(txn);
500500
} catch (Exception e) {
501-
SpannerException spannerException = SpannerExceptionFactory.newSpannerException(e);
501+
SpannerException spannerException = SpannerExceptionFactory.asSpannerException(e);
502502
// Mark multiplexed sessions for RW as unimplemented and fall back to regular sessions
503503
// if UNIMPLEMENTED is returned.
504504
maybeMarkUnimplementedForRW(spannerException);

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static <T> T getOrNull(ApiFuture<T> future) throws SpannerException {
3333
if (e.getCause() instanceof SpannerException) {
3434
throw (SpannerException) e.getCause();
3535
}
36-
throw SpannerExceptionFactory.newSpannerException(e.getCause());
36+
throw SpannerExceptionFactory.asSpannerException(e.getCause());
3737
} catch (InterruptedException e) {
3838
throw SpannerExceptionFactory.propagateInterrupt(e, null /*TODO: requestId*/);
3939
} catch (CancellationException e) {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ public String getResourceName() {
5555
private static final long serialVersionUID = 20150916L;
5656
private static final Metadata.Key<RetryInfo> KEY_RETRY_INFO =
5757
ProtoUtils.keyForProto(RetryInfo.getDefaultInstance());
58+
private static final String PG_ERR_CODE_KEY = "pg_sqlerrcode";
5859

5960
private final ErrorCode code;
6061
private final ApiException apiException;
6162
private XGoogSpannerRequestId requestId;
63+
private String statement;
6264

6365
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
6466
SpannerException(
@@ -99,11 +101,31 @@ public String getResourceName() {
99101
this.requestId = requestId;
100102
}
101103

104+
@Override
105+
public String getMessage() {
106+
if (this.statement == null) {
107+
return super.getMessage();
108+
}
109+
return String.format("%s - Statement: '%s'", super.getMessage(), this.statement);
110+
}
111+
102112
/** Returns the error code associated with this exception. */
103113
public ErrorCode getErrorCode() {
104114
return code;
105115
}
106116

117+
/**
118+
* Returns the PostgreSQL SQLState error code that is encoded in this exception, or null if this
119+
* {@link SpannerException} does not include a PostgreSQL error code.
120+
*/
121+
public String getPostgreSQLErrorCode() {
122+
ErrorDetails details = getErrorDetails();
123+
if (details == null || details.getErrorInfo() == null) {
124+
return null;
125+
}
126+
return details.getErrorInfo().getMetadataOrDefault(PG_ERR_CODE_KEY, null);
127+
}
128+
107129
public String getRequestId() {
108130
if (requestId == null) {
109131
return "";
@@ -199,6 +221,10 @@ public ErrorDetails getErrorDetails() {
199221
return null;
200222
}
201223

224+
void setStatement(String statement) {
225+
this.statement = statement;
226+
}
227+
202228
/** Sets the requestId. */
203229
@InternalApi
204230
public void setRequestId(XGoogSpannerRequestId reqId) {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,10 @@ private static ResourceInfo extractResourceInfo(Throwable cause) {
297297
return null;
298298
}
299299

300-
private static ErrorInfo extractErrorInfo(Throwable cause) {
300+
private static ErrorInfo extractErrorInfo(Throwable cause, ApiException apiException) {
301+
if (apiException != null && apiException.getErrorDetails() != null) {
302+
return apiException.getErrorDetails().getErrorInfo();
303+
}
301304
if (cause != null) {
302305
Metadata trailers = Status.trailersFromThrowable(cause);
303306
if (trailers != null) {
@@ -307,7 +310,11 @@ private static ErrorInfo extractErrorInfo(Throwable cause) {
307310
return null;
308311
}
309312

310-
static ErrorDetails extractErrorDetails(Throwable cause) {
313+
static ErrorDetails extractErrorDetails(Throwable cause, ApiException apiException) {
314+
if (apiException != null && apiException.getErrorDetails() != null) {
315+
return apiException.getErrorDetails();
316+
}
317+
311318
Throwable prevCause = null;
312319
while (cause != null && cause != prevCause) {
313320
if (cause instanceof ApiException) {
@@ -356,7 +363,7 @@ static SpannerException newSpannerExceptionPreformatted(
356363
case ABORTED:
357364
return new AbortedException(token, message, cause, apiException, reqId);
358365
case RESOURCE_EXHAUSTED:
359-
ErrorInfo info = extractErrorInfo(cause);
366+
ErrorInfo info = extractErrorInfo(cause, apiException);
360367
if (info != null
361368
&& info.getMetadataMap()
362369
.containsKey(AdminRequestsPerMinuteExceededException.ADMIN_REQUESTS_LIMIT_KEY)
@@ -382,7 +389,7 @@ static SpannerException newSpannerExceptionPreformatted(
382389
}
383390
}
384391
case INVALID_ARGUMENT:
385-
if (isTransactionMutationLimitException(cause)) {
392+
if (isTransactionMutationLimitException(cause, apiException)) {
386393
return new TransactionMutationLimitExceededException(
387394
token, code, message, cause, apiException, reqId);
388395
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionMutationLimitExceededException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ static boolean isTransactionMutationLimitException(ErrorCode code, String messag
4343
return code == ErrorCode.INVALID_ARGUMENT && message != null && message.contains(ERROR_MESSAGE);
4444
}
4545

46-
static boolean isTransactionMutationLimitException(Throwable cause) {
46+
static boolean isTransactionMutationLimitException(Throwable cause, ApiException apiException) {
4747
if (cause == null
4848
|| cause.getMessage() == null
4949
|| !cause.getMessage().contains(ERROR_MESSAGE)) {
@@ -53,7 +53,7 @@ static boolean isTransactionMutationLimitException(Throwable cause) {
5353
// was that the transaction mutation limit was exceeded. We use that here to identify the error,
5454
// as there is no other specific metadata in the error that identifies it (other than the error
5555
// message).
56-
ErrorDetails errorDetails = extractErrorDetails(cause);
56+
ErrorDetails errorDetails = extractErrorDetails(cause, apiException);
5757
if (errorDetails != null && errorDetails.getHelp() != null) {
5858
return errorDetails.getHelp().getLinksCount() == 1
5959
&& errorDetails

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ void ensureTxn() {
281281
try {
282282
ensureTxnAsync().get();
283283
} catch (ExecutionException e) {
284-
throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause());
284+
throw SpannerExceptionFactory.asSpannerException(e.getCause() == null ? e : e.getCause());
285285
} catch (InterruptedException e) {
286286
throw SpannerExceptionFactory.propagateInterrupt(e);
287287
}
@@ -370,7 +370,7 @@ void commit() {
370370
throw SpannerExceptionFactory.propagateTimeout((TimeoutException) e);
371371
}
372372
} catch (ExecutionException e) {
373-
throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause());
373+
throw SpannerExceptionFactory.asSpannerException(e.getCause() == null ? e : e.getCause());
374374
}
375375
}
376376

@@ -679,7 +679,7 @@ options, getPreviousTransactionId())))
679679
aborted = true;
680680
}
681681
}
682-
throw SpannerExceptionFactory.newSpannerException(e.getCause());
682+
throw SpannerExceptionFactory.asSpannerException(e.getCause());
683683
} catch (TimeoutException e) {
684684
// Throw an ABORTED exception to force a retry of the transaction if no transaction
685685
// has been returned by the first statement.

0 commit comments

Comments
 (0)