Skip to content

Commit cc5ac48

Browse files
authored
feat: include SQL statement in error message (#355)
1 parent cea51ad commit cc5ac48

File tree

4 files changed

+94
-4
lines changed

4 files changed

+94
-4
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ ExecuteBatchDmlRequest.Builder getExecuteBatchDmlRequestBuilder(Iterable<Stateme
607607
}
608608

609609
ResultSet executeQueryInternalWithOptions(
610-
Statement statement,
610+
final Statement statement,
611611
com.google.spanner.v1.ExecuteSqlRequest.QueryMode queryMode,
612612
Options options,
613613
ByteString partitionToken) {
@@ -622,7 +622,7 @@ ResultSet executeQueryInternalWithOptions(
622622
new ResumableStreamIterator(MAX_BUFFERED_CHUNKS, SpannerImpl.QUERY, span) {
623623
@Override
624624
CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken) {
625-
GrpcStreamIterator stream = new GrpcStreamIterator(prefetchChunks);
625+
GrpcStreamIterator stream = new GrpcStreamIterator(statement, prefetchChunks);
626626
if (resumeToken != null) {
627627
request.setResumeToken(resumeToken);
628628
}

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

+26-1
Original file line numberDiff line numberDiff line change
@@ -763,16 +763,24 @@ interface CloseableIterator<T> extends Iterator<T> {
763763
@VisibleForTesting
764764
static class GrpcStreamIterator extends AbstractIterator<PartialResultSet>
765765
implements CloseableIterator<PartialResultSet> {
766+
private static final Logger logger = Logger.getLogger(GrpcStreamIterator.class.getName());
766767
private static final PartialResultSet END_OF_STREAM = PartialResultSet.newBuilder().build();
767768

768769
private final ConsumerImpl consumer = new ConsumerImpl();
769770
private final BlockingQueue<PartialResultSet> stream;
771+
private final Statement statement;
770772

771773
private SpannerRpc.StreamingCall call;
772774
private SpannerException error;
773775

774-
// Visible for testing.
776+
@VisibleForTesting
775777
GrpcStreamIterator(int prefetchChunks) {
778+
this(null, prefetchChunks);
779+
}
780+
781+
@VisibleForTesting
782+
GrpcStreamIterator(Statement statement, int prefetchChunks) {
783+
this.statement = statement;
776784
// One extra to allow for END_OF_STREAM message.
777785
this.stream = new LinkedBlockingQueue<>(prefetchChunks + 1);
778786
}
@@ -839,6 +847,23 @@ public void onCompleted() {
839847

840848
@Override
841849
public void onError(SpannerException e) {
850+
if (statement != null) {
851+
if (logger.isLoggable(Level.FINEST)) {
852+
// Include parameter values if logging level is set to FINEST or higher.
853+
e =
854+
SpannerExceptionFactory.newSpannerExceptionPreformatted(
855+
e.getErrorCode(),
856+
String.format("%s - Statement: '%s'", e.getMessage(), statement.toString()),
857+
e);
858+
logger.log(Level.FINEST, "Error executing statement", e);
859+
} else {
860+
e =
861+
SpannerExceptionFactory.newSpannerExceptionPreformatted(
862+
e.getErrorCode(),
863+
String.format("%s - Statement: '%s'", e.getMessage(), statement.getSql()),
864+
e);
865+
}
866+
}
842867
error = e;
843868
addToStream(END_OF_STREAM);
844869
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ private static ResourceInfo extractResourceInfo(Throwable cause) {
188188
return null;
189189
}
190190

191-
private static SpannerException newSpannerExceptionPreformatted(
191+
static SpannerException newSpannerExceptionPreformatted(
192192
ErrorCode code, @Nullable String message, @Nullable Throwable cause) {
193193
// This is the one place in the codebase that is allowed to call constructors directly.
194194
DoNotConstructDirectly token = DoNotConstructDirectly.ALLOWED;

google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

+65
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.api.gax.grpc.testing.LocalChannelProvider;
2626
import com.google.api.gax.retrying.RetrySettings;
2727
import com.google.cloud.NoCredentials;
28+
import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator;
2829
import com.google.cloud.spanner.AsyncResultSet.CallbackResponse;
2930
import com.google.cloud.spanner.AsyncResultSet.ReadyCallback;
3031
import com.google.cloud.spanner.AsyncRunner.AsyncWork;
@@ -33,6 +34,7 @@
3334
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
3435
import com.google.cloud.spanner.TransactionRunner.TransactionCallable;
3536
import com.google.common.base.Stopwatch;
37+
import com.google.common.collect.ImmutableList;
3638
import com.google.common.util.concurrent.SettableFuture;
3739
import com.google.protobuf.AbstractMessage;
3840
import com.google.spanner.v1.ExecuteSqlRequest;
@@ -52,6 +54,8 @@
5254
import java.util.concurrent.ScheduledThreadPoolExecutor;
5355
import java.util.concurrent.TimeUnit;
5456
import java.util.concurrent.atomic.AtomicInteger;
57+
import java.util.logging.Level;
58+
import java.util.logging.Logger;
5559
import org.junit.After;
5660
import org.junit.AfterClass;
5761
import org.junit.Before;
@@ -1483,4 +1487,65 @@ public void testBatchCreateSessionsPermissionDenied() {
14831487
}
14841488
}
14851489
}
1490+
1491+
@Test
1492+
public void testExceptionIncludesStatement() {
1493+
mockSpanner.setExecuteStreamingSqlExecutionTime(
1494+
SimulatedExecutionTime.ofException(
1495+
Status.INVALID_ARGUMENT.withDescription("Invalid query").asRuntimeException()));
1496+
DatabaseClient client =
1497+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
1498+
try (ResultSet rs =
1499+
client
1500+
.singleUse()
1501+
.executeQuery(
1502+
Statement.newBuilder("SELECT * FROM FOO WHERE ID=@id").bind("id").to(1L).build())) {
1503+
rs.next();
1504+
fail("missing expected exception");
1505+
} catch (SpannerException e) {
1506+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
1507+
assertThat(e.getMessage()).contains("Statement: 'SELECT * FROM FOO WHERE ID=@id'");
1508+
// The error message should normally not include the parameter values to prevent sensitive
1509+
// information from accidentally being logged.
1510+
assertThat(e.getMessage()).doesNotContain("id: 1");
1511+
}
1512+
1513+
mockSpanner.setExecuteStreamingSqlExecutionTime(
1514+
SimulatedExecutionTime.ofException(
1515+
Status.INVALID_ARGUMENT.withDescription("Invalid query").asRuntimeException()));
1516+
Logger logger = Logger.getLogger(GrpcStreamIterator.class.getName());
1517+
Level currentLevel = logger.getLevel();
1518+
try (ResultSet rs =
1519+
client
1520+
.singleUse()
1521+
.executeQuery(
1522+
Statement.newBuilder("SELECT * FROM FOO WHERE ID=@id").bind("id").to(1L).build())) {
1523+
logger.setLevel(Level.FINEST);
1524+
rs.next();
1525+
fail("missing expected exception");
1526+
} catch (SpannerException e) {
1527+
// With log level set to FINEST the error should also include the parameter values.
1528+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
1529+
assertThat(e.getMessage()).contains("Statement: 'SELECT * FROM FOO WHERE ID=@id {id: 1}'");
1530+
} finally {
1531+
logger.setLevel(currentLevel);
1532+
}
1533+
}
1534+
1535+
@Test
1536+
public void testReadDoesNotIncludeStatement() {
1537+
mockSpanner.setStreamingReadExecutionTime(
1538+
SimulatedExecutionTime.ofException(
1539+
Status.INVALID_ARGUMENT.withDescription("Invalid read").asRuntimeException()));
1540+
DatabaseClient client =
1541+
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
1542+
try (ResultSet rs =
1543+
client.singleUse().read("FOO", KeySet.singleKey(Key.of(1L)), ImmutableList.of("BAR"))) {
1544+
rs.next();
1545+
fail("missing expected exception");
1546+
} catch (SpannerException e) {
1547+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
1548+
assertThat(e.getMessage()).doesNotContain("Statement:");
1549+
}
1550+
}
14861551
}

0 commit comments

Comments
 (0)