Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Array conversion to ResultSet #326

Merged
merged 6 commits into from Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 78 additions & 7 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
Expand Up @@ -16,11 +16,21 @@

package com.google.cloud.spanner.jdbc;

import com.google.cloud.ByteArray;
import com.google.cloud.spanner.ResultSets;
import com.google.cloud.spanner.Struct;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.StructField;
import com.google.cloud.spanner.ValueBinder;
import com.google.common.collect.ImmutableList;
import com.google.rpc.Code;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -137,28 +147,89 @@ public Object getArray(long index, int count, Map<String, Class<?>> map) throws
return null;
}

private static final String RESULTSET_NOT_SUPPORTED =
"Getting a ResultSet from an array is not supported";
private static final String RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED =
"Getting a ResultSet with a custom type mapping from an array is not supported";

@Override
public ResultSet getResultSet() throws SQLException {
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
return getResultSet(1L, Integer.MAX_VALUE);
}

@Override
public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
}

@Override
public ResultSet getResultSet(long index, int count) throws SQLException {
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
public ResultSet getResultSet(long startIndex, int count) throws SQLException {
JdbcPreconditions.checkArgument(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just Preconditions and an IllegalArgumentException?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the JDBC specification requires the driver to throw an instance of SQLException if the input is invalid. This applies to (almost) all methods defined by the JDBC specification, hence the separate JdbcPreconditions helper class.

startIndex + count - 1L <= Integer.MAX_VALUE,
String.format("End index cannot exceed %d", Integer.MAX_VALUE));
JdbcPreconditions.checkArgument(startIndex >= 1L, "Start index must be >= 1");
JdbcPreconditions.checkArgument(count >= 0, "Count must be >= 0");
checkFree();
ImmutableList.Builder<Struct> rows = ImmutableList.builder();
int added = 0;
if (data != null) {
// Note that array index in JDBC is base-one.
for (int index = (int) startIndex;
added < count && index <= ((Object[]) data).length;
index++) {
Object value = ((Object[]) data)[index - 1];
ValueBinder<Struct.Builder> binder =
Struct.newBuilder().set("INDEX").to(index).set("VALUE");
Struct.Builder builder = null;
switch (type.getCode()) {
case BOOL:
builder = binder.to((Boolean) value);
break;
case BYTES:
builder = binder.to(ByteArray.copyFrom((byte[]) value));
break;
case DATE:
builder = binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
break;
case FLOAT64:
builder = binder.to((Double) value);
break;
case INT64:
builder = binder.to((Long) value);
break;
case NUMERIC:
builder = binder.to((BigDecimal) value);
break;
case STRING:
builder = binder.to((String) value);
break;
case TIMESTAMP:
builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
break;
case ARRAY:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we create tests for the ARRAY, STRUCT and default cases?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not really possible, because it is impossible to create an array with an invalid data type. It will fail already during creation, but I've added a couple of tests to verify that.

case STRUCT:
default:
throw new SQLFeatureNotSupportedException(
String.format(
"Array of type %s cannot be converted to a ResultSet", type.getCode().name()));
}
rows.add(builder.build());
added++;
if (added == count) {
break;
}
}
}
return JdbcResultSet.of(
ResultSets.forRows(
Type.struct(
StructField.of("INDEX", Type.int64()),
StructField.of("VALUE", type.getSpannerType())),
rows.build()));
}

@Override
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map)
throws SQLException {
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
}

@Override
Expand Down
186 changes: 166 additions & 20 deletions src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java
Expand Up @@ -16,10 +16,16 @@

package com.google.cloud.spanner.jdbc;

import static org.junit.Assert.assertEquals;
import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.toSqlDate;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
Expand All @@ -35,42 +41,182 @@ public void testCreateArrayTypeName() throws SQLException {
// Note that JDBC array indices start at 1.
JdbcArray array;
array = JdbcArray.createArray("BOOL", new Boolean[] {true, false, true});
assertEquals(array.getBaseType(), Types.BOOLEAN);
assertEquals(((Boolean[]) array.getArray(1, 1))[0], Boolean.TRUE);
assertThat(array.getBaseType()).isEqualTo(Types.BOOLEAN);
assertThat(((Boolean[]) array.getArray(1, 1))[0]).isEqualTo(Boolean.TRUE);
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just assertEquals and assertTrue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library (and also the Java Spanner client) mostly use Truth for assertions, but some of the older classes use JUnit assertions. To reduce the mixing of the two, I tend to change the older classes to Truth when (larger) changes are made to them.

assertThat(rs.getBoolean(2)).isEqualTo(true);
assertThat(rs.next()).isTrue();
assertThat(rs.getBoolean(2)).isEqualTo(false);
assertThat(rs.next()).isTrue();
assertThat(rs.getBoolean(2)).isEqualTo(true);
assertThat(rs.next()).isFalse();
}

array = JdbcArray.createArray("BYTES", new byte[][] {new byte[] {1, 2}, new byte[] {3, 4}});
assertEquals(array.getBaseType(), Types.BINARY);
assertEquals(((byte[][]) array.getArray(1, 1))[0][1], (byte) 2);
assertThat(array.getBaseType()).isEqualTo(Types.BINARY);
assertThat(((byte[][]) array.getArray(1, 1))[0][1]).isEqualTo((byte) 2);
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getBytes(2)).isEqualTo(new byte[] {1, 2});
assertThat(rs.next()).isTrue();
assertThat(rs.getBytes(2)).isEqualTo(new byte[] {3, 4});
assertThat(rs.next()).isFalse();
}

array =
JdbcArray.createArray("DATE", new Date[] {new Date(1L), new Date(100L), new Date(1000L)});
assertEquals(array.getBaseType(), Types.DATE);
assertEquals(((Date[]) array.getArray(1, 1))[0], new Date(1L));
JdbcArray.createArray(
"DATE",
new Date[] {
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)),
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)),
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31))
});
assertThat(array.getBaseType()).isEqualTo(Types.DATE);
assertThat(((Date[]) array.getArray(1, 1))[0])
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getDate(2))
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
assertThat(rs.next()).isTrue();
assertThat(rs.getDate(2))
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)));
assertThat(rs.next()).isTrue();
assertThat(rs.getDate(2))
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31)));
assertThat(rs.next()).isFalse();
}

array = JdbcArray.createArray("FLOAT64", new Double[] {1.1D, 2.2D, Math.PI});
assertEquals(array.getBaseType(), Types.DOUBLE);
assertEquals(((Double[]) array.getArray(1, 3))[2], Double.valueOf(Math.PI));
assertThat(array.getBaseType()).isEqualTo(Types.DOUBLE);
assertThat(((Double[]) array.getArray(1, 3))[2]).isEqualTo(Double.valueOf(Math.PI));
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getDouble(2)).isEqualTo(1.1D);
assertThat(rs.next()).isTrue();
assertThat(rs.getDouble(2)).isEqualTo(2.2D);
assertThat(rs.next()).isTrue();
assertThat(rs.getDouble(2)).isEqualTo(Math.PI);
assertThat(rs.next()).isFalse();
}

array = JdbcArray.createArray("INT64", new Long[] {1L, 2L, 3L});
assertEquals(array.getBaseType(), Types.BIGINT);
assertEquals(((Long[]) array.getArray(1, 1))[0], Long.valueOf(1L));
assertThat(array.getBaseType()).isEqualTo(Types.BIGINT);
assertThat(((Long[]) array.getArray(1, 1))[0]).isEqualTo(Long.valueOf(1L));
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getLong(2)).isEqualTo(1L);
assertThat(rs.next()).isTrue();
assertThat(rs.getLong(2)).isEqualTo(2L);
assertThat(rs.next()).isTrue();
assertThat(rs.getLong(2)).isEqualTo(3L);
assertThat(rs.next()).isFalse();
}

array =
JdbcArray.createArray("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN});
assertEquals(array.getBaseType(), Types.NUMERIC);
assertEquals(((BigDecimal[]) array.getArray(1, 1))[0], BigDecimal.ONE);
assertEquals(((BigDecimal[]) array.getArray(2, 1))[0], null);
assertEquals(((BigDecimal[]) array.getArray(3, 1))[0], BigDecimal.TEN);
assertThat(array.getBaseType()).isEqualTo(Types.NUMERIC);
assertThat(((BigDecimal[]) array.getArray(1, 1))[0]).isEqualTo(BigDecimal.ONE);
assertThat(((BigDecimal[]) array.getArray(2, 1))[0]).isNull();
assertThat(((BigDecimal[]) array.getArray(3, 1))[0]).isEqualTo(BigDecimal.TEN);
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.ONE);
assertThat(rs.next()).isTrue();
assertThat(rs.getBigDecimal(2)).isNull();
assertThat(rs.next()).isTrue();
assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.TEN);
assertThat(rs.next()).isFalse();
}

array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
assertEquals(array.getBaseType(), Types.NVARCHAR);
assertEquals(((String[]) array.getArray(1, 1))[0], "foo");
assertThat(array.getBaseType()).isEqualTo(Types.NVARCHAR);
assertThat(((String[]) array.getArray(1, 1))[0]).isEqualTo("foo");
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("foo");
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("bar");
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("baz");
assertThat(rs.next()).isFalse();
}

array =
JdbcArray.createArray(
"TIMESTAMP",
new Timestamp[] {new Timestamp(1L), new Timestamp(100L), new Timestamp(1000L)});
assertEquals(array.getBaseType(), Types.TIMESTAMP);
assertEquals(((Timestamp[]) array.getArray(1, 1))[0], new Timestamp(1L));
assertThat(array.getBaseType()).isEqualTo(Types.TIMESTAMP);
assertThat(((Timestamp[]) array.getArray(1, 1))[0]).isEqualTo(new Timestamp(1L));
try (ResultSet rs = array.getResultSet()) {
assertThat(rs.next()).isTrue();
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1L));
assertThat(rs.next()).isTrue();
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(100L));
assertThat(rs.next()).isTrue();
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1000L));
assertThat(rs.next()).isFalse();
}
}

@Test
public void testGetResultSetMetadata() throws SQLException {
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
try (ResultSet rs = array.getResultSet()) {
ResultSetMetaData metadata = rs.getMetaData();
assertThat(metadata.getColumnCount()).isEqualTo(2);
assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT);
assertThat(metadata.getColumnType(2)).isEqualTo(Types.NVARCHAR);
assertThat(metadata.getColumnName(1)).isEqualTo("INDEX");
assertThat(metadata.getColumnName(2)).isEqualTo("VALUE");
}
}

@Test
public void testGetResultSetWithIndex() throws SQLException {
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
try (ResultSet rs = array.getResultSet(2L, 1)) {
assertThat(rs.next()).isTrue();
assertThat(rs.getLong("INDEX")).isEqualTo(2L);
assertThat(rs.getString("VALUE")).isEqualTo("bar");
assertThat(rs.next()).isFalse();
}

try (ResultSet rs = array.getResultSet(1L, 5)) {
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("foo");
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("bar");
assertThat(rs.next()).isTrue();
assertThat(rs.getString(2)).isEqualTo("baz");
assertThat(rs.next()).isFalse();
}

try (ResultSet rs = array.getResultSet(1L, 0)) {
assertThat(rs.next()).isFalse();
}
}

@Test
public void testGetResultSetWithInvalidIndex() throws SQLException {
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
try (ResultSet rs = array.getResultSet(0L, 1)) {
fail("missing expected exception");
} catch (JdbcSqlExceptionImpl e) {
assertThat(e.getErrorCode())
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
}
}

@Test
public void testGetResultSetWithInvalidCount() throws SQLException {
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
try (ResultSet rs = array.getResultSet(1L, -1)) {
fail("missing expected exception");
} catch (JdbcSqlExceptionImpl e) {
assertThat(e.getErrorCode())
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
}
}
}