diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index c2af543cc9b1..7f9c2c7f324c 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -329,7 +329,8 @@ private void putString(String stringValue) { if (buffer == null || (buffer.capacity() < MAX_BUFFER_SIZE && buffer.capacity() < length)) { // Create a ByteBuffer with a maximum buffer size. // This buffer is re-used for all string values in the result set. - buffer = ByteBuffer.allocate(Math.min(MAX_BUFFER_SIZE, length)); + // UTF-8 can require 4 bytes to represent, so we need at least that size buffer. + buffer = ByteBuffer.allocate(Math.max(4, Math.min(MAX_BUFFER_SIZE, length))); } else { buffer.clear(); } @@ -349,8 +350,8 @@ private void putString(String stringValue) { buffer.flip(); // Put the bytes from the buffer into the digest. digest.update(buffer); - // Flip the buffer again, so we can repeat and write to the start of the buffer again. - buffer.flip(); + // Clear the buffer, so we can repeat and write to the start of the buffer again. + buffer.clear(); } } } diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java index 6201200ec076..f79e33962a9d 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java @@ -448,4 +448,42 @@ public void testRetry() { () -> resultSet.retry(abortedException)); } } + + @Test + public void testEmptyString() { + ChecksumResultSet resultSet = createStringValChecksumResultSet(""); + assertTrue(resultSet.next()); + } + + @Test + public void testSingleCharMultiByteString() { + ChecksumResultSet resultSet = createStringValChecksumResultSet("รค"); + assertTrue(resultSet.next()); + } + + @Test + public void testLongMixedUtf8String() { + ChecksumResultSet resultSet = createStringValChecksumResultSet("aaa\uD841\uDF0E"); + assertTrue(resultSet.next()); + } + + private ChecksumResultSet createStringValChecksumResultSet(String value) { + Type type = Type.struct(StructField.of("stringVal", Type.string())); + Struct row = Struct.newBuilder().set("stringVal").to(value).build(); + + ParsedStatement parsedStatement = mock(ParsedStatement.class); + Statement statement = Statement.of("select * from foo"); + when(parsedStatement.getStatement()).thenReturn(statement); + ReadWriteTransaction transaction = mock(ReadWriteTransaction.class); + when(transaction.runWithRetry(any(Callable.class))) + .thenAnswer(invocationOnMock -> ((Callable) invocationOnMock.getArgument(0)).call()); + when(transaction.getStatementExecutor()).thenReturn(mock(StatementExecutor.class)); + + ResultSet queryResult = ResultSets.forRows(type, ImmutableList.of(row)); + return new ChecksumResultSet( + transaction, + DirectExecuteResultSet.ofResultSet(queryResult), + parsedStatement, + AnalyzeMode.NONE); + } }