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
Changes from all commits
35859a9
066ae3f
3108c4b
2e6293d
562b60c
8a46d00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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( | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we create tests for the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -35,42 +41,208 @@ 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just assertEquals and assertTrue? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 testCreateArrayOfArray() { | ||
try { | ||
JdbcArray.createArray("ARRAY<STRING>", new String[][] {{}}); | ||
fail("missing expected exception"); | ||
} catch (SQLException e) { | ||
assertThat((Exception) e).isInstanceOf(JdbcSqlException.class); | ||
JdbcSqlException jse = (JdbcSqlException) e; | ||
assertThat(jse.getErrorCode()) | ||
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value()); | ||
} | ||
} | ||
|
||
@Test | ||
public void testCreateArrayOfStruct() { | ||
try { | ||
JdbcArray.createArray("STRUCT", new Object[] {}); | ||
fail("missing expected exception"); | ||
} catch (SQLException e) { | ||
assertThat((Exception) e).isInstanceOf(JdbcSqlException.class); | ||
JdbcSqlException jse = (JdbcSqlException) e; | ||
assertThat(jse.getErrorCode()) | ||
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value()); | ||
} | ||
} | ||
|
||
@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()); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 separateJdbcPreconditions
helper class.