diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java index 1ea58b2bc6..afe2fdb246 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java @@ -59,7 +59,7 @@ private R runTransaction(final AsyncWork work) { try { return work.doWorkAsync(transaction).get(); } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause()); } catch (InterruptedException e) { throw SpannerExceptionFactory.propagateInterrupt(e); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 14089afb82..c394ad09fe 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -123,7 +123,7 @@ private ApiFuture internalBeginAsync( @Override public void onFailure(Throwable t) { onError(t); - res.setException(SpannerExceptionFactory.newSpannerException(t)); + res.setException(SpannerExceptionFactory.asSpannerException(t)); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java index c013c48800..80a9dfcf53 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcResultSet.java @@ -16,7 +16,7 @@ package com.google.cloud.spanner; -import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; +import static com.google.cloud.spanner.SpannerExceptionFactory.asSpannerException; import static com.google.common.base.Preconditions.checkState; import com.google.api.core.InternalApi; @@ -76,7 +76,7 @@ protected GrpcStruct currRow() { @Override public boolean next() throws SpannerException { if (error != null) { - throw newSpannerException(error); + throw asSpannerException(error); } try { if (currRow == null) { @@ -108,7 +108,7 @@ public boolean next() throws SpannerException { return hasNext; } catch (Throwable t) { throw yieldError( - SpannerExceptionFactory.asSpannerException(t), + asSpannerException(t), iterator.isWithBeginTransaction() && currRow == null, iterator.isLastStatement()); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java index 371accb7ee..e0df4c422e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/GrpcStreamIterator.java @@ -155,7 +155,7 @@ protected final PartialResultSet computeNext() { call = null; if (error != null) { - throw SpannerExceptionFactory.newSpannerException(error); + throw SpannerExceptionFactory.asSpannerException(error); } endOfData(); @@ -192,25 +192,17 @@ public void onCompleted() { } @Override - public void onError(SpannerException e) { + public void onError(SpannerException exception) { if (statement != null) { if (logger.isLoggable(Level.FINEST)) { // Include parameter values if logging level is set to FINEST or higher. - e = - SpannerExceptionFactory.newSpannerExceptionPreformatted( - e.getErrorCode(), - String.format("%s - Statement: '%s'", e.getMessage(), statement.toString()), - e); - logger.log(Level.FINEST, "Error executing statement", e); + exception.setStatement(statement.toString()); + logger.log(Level.FINEST, "Error executing statement", exception); } else { - e = - SpannerExceptionFactory.newSpannerExceptionPreformatted( - e.getErrorCode(), - String.format("%s - Statement: '%s'", e.getMessage(), statement.getSql()), - e); + exception.setStatement(statement.getSql()); } } - error = e; + error = exception; addToStream(END_OF_STREAM); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index ef1f4a8894..4b092bc986 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -498,7 +498,7 @@ private void verifyBeginTransactionWithRWOnMultiplexedSessionAsync(String sessio } readWriteBeginTransactionReferenceFuture.set(txn); } catch (Exception e) { - SpannerException spannerException = SpannerExceptionFactory.newSpannerException(e); + SpannerException spannerException = SpannerExceptionFactory.asSpannerException(e); // Mark multiplexed sessions for RW as unimplemented and fall back to regular sessions // if UNIMPLEMENTED is returned. maybeMarkUnimplementedForRW(spannerException); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java index fb55378aa5..7e6acd02f9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerApiFutures.java @@ -33,7 +33,7 @@ public static T getOrNull(ApiFuture future) throws SpannerException { if (e.getCause() instanceof SpannerException) { throw (SpannerException) e.getCause(); } - throw SpannerExceptionFactory.newSpannerException(e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause()); } catch (InterruptedException e) { throw SpannerExceptionFactory.propagateInterrupt(e, null /*TODO: requestId*/); } catch (CancellationException e) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java index c5af3f4815..fbe60e2a1d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java @@ -55,10 +55,12 @@ public String getResourceName() { private static final long serialVersionUID = 20150916L; private static final Metadata.Key KEY_RETRY_INFO = ProtoUtils.keyForProto(RetryInfo.getDefaultInstance()); + private static final String PG_ERR_CODE_KEY = "pg_sqlerrcode"; private final ErrorCode code; private final ApiException apiException; private XGoogSpannerRequestId requestId; + private String statement; /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */ SpannerException( @@ -99,11 +101,31 @@ public String getResourceName() { this.requestId = requestId; } + @Override + public String getMessage() { + if (this.statement == null) { + return super.getMessage(); + } + return String.format("%s - Statement: '%s'", super.getMessage(), this.statement); + } + /** Returns the error code associated with this exception. */ public ErrorCode getErrorCode() { return code; } + /** + * Returns the PostgreSQL SQLState error code that is encoded in this exception, or null if this + * {@link SpannerException} does not include a PostgreSQL error code. + */ + public String getPostgreSQLErrorCode() { + ErrorDetails details = getErrorDetails(); + if (details == null || details.getErrorInfo() == null) { + return null; + } + return details.getErrorInfo().getMetadataOrDefault(PG_ERR_CODE_KEY, null); + } + public String getRequestId() { if (requestId == null) { return ""; @@ -199,6 +221,10 @@ public ErrorDetails getErrorDetails() { return null; } + void setStatement(String statement) { + this.statement = statement; + } + /** Sets the requestId. */ @InternalApi public void setRequestId(XGoogSpannerRequestId reqId) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java index f55770dff9..b9cc1cfb8b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java @@ -297,7 +297,10 @@ private static ResourceInfo extractResourceInfo(Throwable cause) { return null; } - private static ErrorInfo extractErrorInfo(Throwable cause) { + private static ErrorInfo extractErrorInfo(Throwable cause, ApiException apiException) { + if (apiException != null && apiException.getErrorDetails() != null) { + return apiException.getErrorDetails().getErrorInfo(); + } if (cause != null) { Metadata trailers = Status.trailersFromThrowable(cause); if (trailers != null) { @@ -307,7 +310,11 @@ private static ErrorInfo extractErrorInfo(Throwable cause) { return null; } - static ErrorDetails extractErrorDetails(Throwable cause) { + static ErrorDetails extractErrorDetails(Throwable cause, ApiException apiException) { + if (apiException != null && apiException.getErrorDetails() != null) { + return apiException.getErrorDetails(); + } + Throwable prevCause = null; while (cause != null && cause != prevCause) { if (cause instanceof ApiException) { @@ -356,7 +363,7 @@ static SpannerException newSpannerExceptionPreformatted( case ABORTED: return new AbortedException(token, message, cause, apiException, reqId); case RESOURCE_EXHAUSTED: - ErrorInfo info = extractErrorInfo(cause); + ErrorInfo info = extractErrorInfo(cause, apiException); if (info != null && info.getMetadataMap() .containsKey(AdminRequestsPerMinuteExceededException.ADMIN_REQUESTS_LIMIT_KEY) @@ -382,7 +389,7 @@ static SpannerException newSpannerExceptionPreformatted( } } case INVALID_ARGUMENT: - if (isTransactionMutationLimitException(cause)) { + if (isTransactionMutationLimitException(cause, apiException)) { return new TransactionMutationLimitExceededException( token, code, message, cause, apiException, reqId); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionMutationLimitExceededException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionMutationLimitExceededException.java index c51ae96e9f..c89fedd7fe 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionMutationLimitExceededException.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionMutationLimitExceededException.java @@ -43,7 +43,7 @@ static boolean isTransactionMutationLimitException(ErrorCode code, String messag return code == ErrorCode.INVALID_ARGUMENT && message != null && message.contains(ERROR_MESSAGE); } - static boolean isTransactionMutationLimitException(Throwable cause) { + static boolean isTransactionMutationLimitException(Throwable cause, ApiException apiException) { if (cause == null || cause.getMessage() == null || !cause.getMessage().contains(ERROR_MESSAGE)) { @@ -53,7 +53,7 @@ static boolean isTransactionMutationLimitException(Throwable cause) { // was that the transaction mutation limit was exceeded. We use that here to identify the error, // as there is no other specific metadata in the error that identifies it (other than the error // message). - ErrorDetails errorDetails = extractErrorDetails(cause); + ErrorDetails errorDetails = extractErrorDetails(cause, apiException); if (errorDetails != null && errorDetails.getHelp() != null) { return errorDetails.getHelp().getLinksCount() == 1 && errorDetails diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index 354d8a7151..6ce8cc11aa 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -281,7 +281,7 @@ void ensureTxn() { try { ensureTxnAsync().get(); } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause() == null ? e : e.getCause()); } catch (InterruptedException e) { throw SpannerExceptionFactory.propagateInterrupt(e); } @@ -370,7 +370,7 @@ void commit() { throw SpannerExceptionFactory.propagateTimeout((TimeoutException) e); } } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause() == null ? e : e.getCause()); } } @@ -679,7 +679,7 @@ options, getPreviousTransactionId()))) aborted = true; } } - throw SpannerExceptionFactory.newSpannerException(e.getCause()); + throw SpannerExceptionFactory.asSpannerException(e.getCause()); } catch (TimeoutException e) { // Throw an ABORTED exception to force a retry of the transaction if no transaction // has been returned by the first statement. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PartitionId.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PartitionId.java index 2690278f3a..2adc264dc6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PartitionId.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PartitionId.java @@ -74,7 +74,7 @@ protected Class resolveClass(ObjectStreamClass desc) throw SpannerExceptionFactory.newSpannerException( ErrorCode.INVALID_ARGUMENT, invalidClassException.getMessage(), invalidClassException); } catch (Exception exception) { - throw SpannerExceptionFactory.newSpannerException(exception); + throw SpannerExceptionFactory.asSpannerException(exception); } } @@ -90,7 +90,7 @@ public static String encodeToString(BatchTransactionId transactionId, Partition new ObjectOutputStream(new GZIPOutputStream(byteArrayOutputStream))) { objectOutputStream.writeObject(id); } catch (Exception exception) { - throw SpannerExceptionFactory.newSpannerException(exception); + throw SpannerExceptionFactory.asSpannerException(exception); } return Base64.getUrlEncoder().encodeToString(byteArrayOutputStream.toByteArray()); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index cc375a98d3..371d73a0af 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner.spi.v1; +import static com.google.cloud.spanner.SpannerExceptionFactory.asSpannerException; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; import static com.google.cloud.spanner.ThreadFactoryUtil.tryCreateVirtualThreadPerTaskExecutor; @@ -538,7 +539,7 @@ public UnaryCallable createUnaryCalla // is actually running. checkEmulatorConnection(options, channelProvider, credentialsProvider, emulatorHost); } catch (Exception e) { - throw newSpannerException(e); + throw asSpannerException(e); } } else { this.databaseAdminStub = null; @@ -726,7 +727,7 @@ private T runWithRetryOnAdministrativeRequestsExceeded(Callable callable) new AdminRequestsLimitExceededRetryAlgorithm<>(), NanoClock.getDefaultClock()); } catch (RetryHelperException e) { - throw SpannerExceptionFactory.asSpannerException(e.getCause()); + throw asSpannerException(e.getCause()); } } @@ -1317,7 +1318,7 @@ public OperationFuture updateDatabaseDdl( throw newSpannerException(e); } catch (ExecutionException e) { Throwable t = e.getCause(); - SpannerException se = SpannerExceptionFactory.asSpannerException(t); + SpannerException se = asSpannerException(t); if (se instanceof AdminRequestsPerMinuteExceededException) { // Propagate this to trigger a retry. throw se; @@ -1983,8 +1984,12 @@ private static T get(final Future future) throws SpannerException { // We are the sole consumer of the future, so cancel it. future.cancel(true); throw SpannerExceptionFactory.propagateInterrupt(e); - } catch (Exception e) { + } catch (ExecutionException e) { + throw asSpannerException(e.getCause()); + } catch (CancellationException e) { throw newSpannerException(context, e, null); + } catch (Exception exception) { + throw asSpannerException(exception); } } @@ -2222,7 +2227,7 @@ public void onError(Throwable t) { if (this.consumer.cancelQueryWhenClientIsClosed()) { unregisterResponseObserver(this); } - consumer.onError(newSpannerException(t)); + consumer.onError(asSpannerException(t)); } @Override diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index d24a3ca101..83140bccda 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -22,6 +22,7 @@ import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -131,16 +132,21 @@ public void simple() { @Test public void badQuery() { - try { - execute(Statement.of("SELECT Apples AND Oranges"), Type.int64()); - fail("Expected exception"); - } catch (SpannerException ex) { - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); - if (dialect.dialect == Dialect.POSTGRESQL) { - assertThat(ex.getMessage()).contains("column \"apples\" does not exist"); - } else { - assertThat(ex.getMessage()).contains("Unrecognized name: Apples"); - } + SpannerException exception = + assertThrows( + SpannerException.class, + () -> execute(Statement.of("SELECT Apples AND Oranges"), Type.int64())); + assertEquals(ErrorCode.INVALID_ARGUMENT, exception.getErrorCode()); + if (dialect.dialect == Dialect.POSTGRESQL) { + assertTrue( + exception.getMessage(), + exception.getMessage().contains("column \"apples\" does not exist")); + // See https://www.postgresql.org/docs/current/errcodes-appendix.html + // '42703' == undefined_column + assertEquals("42703", exception.getPostgreSQLErrorCode()); + } else { + assertTrue( + exception.getMessage(), exception.getMessage().contains("Unrecognized name: Apples")); } }