Skip to content

Commit 6ea0a26

Browse files
authored
feat: Support Array conversion to ResultSet (#326)
* feat: add support for array as ResultSet * feat: add support for array as ResultSet JDBC arrays can optionally be converted to ResultSets. This feature was not implemented for the Cloud Spanner JDBC driver. This would cause DBeaver to show an error message instead of the actual data when fetching a NUMERIC array. Other arrays would be fetched correctly by DBeaver, as those array types do not use the conversion to a ResultSet. * test: add tests for invalid data types
1 parent 3060b6b commit 6ea0a26

File tree

2 files changed

+270
-27
lines changed

2 files changed

+270
-27
lines changed

src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@
1616

1717
package com.google.cloud.spanner.jdbc;
1818

19+
import com.google.cloud.ByteArray;
20+
import com.google.cloud.spanner.ResultSets;
21+
import com.google.cloud.spanner.Struct;
22+
import com.google.cloud.spanner.Type;
23+
import com.google.cloud.spanner.Type.StructField;
24+
import com.google.cloud.spanner.ValueBinder;
25+
import com.google.common.collect.ImmutableList;
1926
import com.google.rpc.Code;
27+
import java.math.BigDecimal;
2028
import java.sql.Array;
29+
import java.sql.Date;
2130
import java.sql.ResultSet;
2231
import java.sql.SQLException;
2332
import java.sql.SQLFeatureNotSupportedException;
33+
import java.sql.Timestamp;
2434
import java.util.Arrays;
2535
import java.util.List;
2636
import java.util.Map;
@@ -137,28 +147,89 @@ public Object getArray(long index, int count, Map<String, Class<?>> map) throws
137147
return null;
138148
}
139149

140-
private static final String RESULTSET_NOT_SUPPORTED =
141-
"Getting a ResultSet from an array is not supported";
150+
private static final String RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED =
151+
"Getting a ResultSet with a custom type mapping from an array is not supported";
142152

143153
@Override
144154
public ResultSet getResultSet() throws SQLException {
145-
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
155+
return getResultSet(1L, Integer.MAX_VALUE);
146156
}
147157

148158
@Override
149159
public ResultSet getResultSet(Map<String, Class<?>> map) throws SQLException {
150-
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
160+
throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
151161
}
152162

153163
@Override
154-
public ResultSet getResultSet(long index, int count) throws SQLException {
155-
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
164+
public ResultSet getResultSet(long startIndex, int count) throws SQLException {
165+
JdbcPreconditions.checkArgument(
166+
startIndex + count - 1L <= Integer.MAX_VALUE,
167+
String.format("End index cannot exceed %d", Integer.MAX_VALUE));
168+
JdbcPreconditions.checkArgument(startIndex >= 1L, "Start index must be >= 1");
169+
JdbcPreconditions.checkArgument(count >= 0, "Count must be >= 0");
170+
checkFree();
171+
ImmutableList.Builder<Struct> rows = ImmutableList.builder();
172+
int added = 0;
173+
if (data != null) {
174+
// Note that array index in JDBC is base-one.
175+
for (int index = (int) startIndex;
176+
added < count && index <= ((Object[]) data).length;
177+
index++) {
178+
Object value = ((Object[]) data)[index - 1];
179+
ValueBinder<Struct.Builder> binder =
180+
Struct.newBuilder().set("INDEX").to(index).set("VALUE");
181+
Struct.Builder builder = null;
182+
switch (type.getCode()) {
183+
case BOOL:
184+
builder = binder.to((Boolean) value);
185+
break;
186+
case BYTES:
187+
builder = binder.to(ByteArray.copyFrom((byte[]) value));
188+
break;
189+
case DATE:
190+
builder = binder.to(JdbcTypeConverter.toGoogleDate((Date) value));
191+
break;
192+
case FLOAT64:
193+
builder = binder.to((Double) value);
194+
break;
195+
case INT64:
196+
builder = binder.to((Long) value);
197+
break;
198+
case NUMERIC:
199+
builder = binder.to((BigDecimal) value);
200+
break;
201+
case STRING:
202+
builder = binder.to((String) value);
203+
break;
204+
case TIMESTAMP:
205+
builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
206+
break;
207+
case ARRAY:
208+
case STRUCT:
209+
default:
210+
throw new SQLFeatureNotSupportedException(
211+
String.format(
212+
"Array of type %s cannot be converted to a ResultSet", type.getCode().name()));
213+
}
214+
rows.add(builder.build());
215+
added++;
216+
if (added == count) {
217+
break;
218+
}
219+
}
220+
}
221+
return JdbcResultSet.of(
222+
ResultSets.forRows(
223+
Type.struct(
224+
StructField.of("INDEX", Type.int64()),
225+
StructField.of("VALUE", type.getSpannerType())),
226+
rows.build()));
156227
}
157228

158229
@Override
159230
public ResultSet getResultSet(long index, int count, Map<String, Class<?>> map)
160231
throws SQLException {
161-
throw new SQLFeatureNotSupportedException(RESULTSET_NOT_SUPPORTED);
232+
throw new SQLFeatureNotSupportedException(RESULTSET_WITH_TYPE_MAPPING_NOT_SUPPORTED);
162233
}
163234

164235
@Override

src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java

Lines changed: 192 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

1717
package com.google.cloud.spanner.jdbc;
1818

19-
import static org.junit.Assert.assertEquals;
19+
import static com.google.cloud.spanner.jdbc.JdbcTypeConverter.toSqlDate;
20+
import static com.google.common.truth.Truth.assertThat;
21+
import static org.junit.Assert.fail;
2022

23+
import com.google.cloud.spanner.ErrorCode;
24+
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
2125
import java.math.BigDecimal;
2226
import java.sql.Date;
27+
import java.sql.ResultSet;
28+
import java.sql.ResultSetMetaData;
2329
import java.sql.SQLException;
2430
import java.sql.Timestamp;
2531
import java.sql.Types;
@@ -35,42 +41,208 @@ public void testCreateArrayTypeName() throws SQLException {
3541
// Note that JDBC array indices start at 1.
3642
JdbcArray array;
3743
array = JdbcArray.createArray("BOOL", new Boolean[] {true, false, true});
38-
assertEquals(array.getBaseType(), Types.BOOLEAN);
39-
assertEquals(((Boolean[]) array.getArray(1, 1))[0], Boolean.TRUE);
44+
assertThat(array.getBaseType()).isEqualTo(Types.BOOLEAN);
45+
assertThat(((Boolean[]) array.getArray(1, 1))[0]).isEqualTo(Boolean.TRUE);
46+
try (ResultSet rs = array.getResultSet()) {
47+
assertThat(rs.next()).isTrue();
48+
assertThat(rs.getBoolean(2)).isEqualTo(true);
49+
assertThat(rs.next()).isTrue();
50+
assertThat(rs.getBoolean(2)).isEqualTo(false);
51+
assertThat(rs.next()).isTrue();
52+
assertThat(rs.getBoolean(2)).isEqualTo(true);
53+
assertThat(rs.next()).isFalse();
54+
}
4055

4156
array = JdbcArray.createArray("BYTES", new byte[][] {new byte[] {1, 2}, new byte[] {3, 4}});
42-
assertEquals(array.getBaseType(), Types.BINARY);
43-
assertEquals(((byte[][]) array.getArray(1, 1))[0][1], (byte) 2);
57+
assertThat(array.getBaseType()).isEqualTo(Types.BINARY);
58+
assertThat(((byte[][]) array.getArray(1, 1))[0][1]).isEqualTo((byte) 2);
59+
try (ResultSet rs = array.getResultSet()) {
60+
assertThat(rs.next()).isTrue();
61+
assertThat(rs.getBytes(2)).isEqualTo(new byte[] {1, 2});
62+
assertThat(rs.next()).isTrue();
63+
assertThat(rs.getBytes(2)).isEqualTo(new byte[] {3, 4});
64+
assertThat(rs.next()).isFalse();
65+
}
4466

4567
array =
46-
JdbcArray.createArray("DATE", new Date[] {new Date(1L), new Date(100L), new Date(1000L)});
47-
assertEquals(array.getBaseType(), Types.DATE);
48-
assertEquals(((Date[]) array.getArray(1, 1))[0], new Date(1L));
68+
JdbcArray.createArray(
69+
"DATE",
70+
new Date[] {
71+
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)),
72+
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)),
73+
toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31))
74+
});
75+
assertThat(array.getBaseType()).isEqualTo(Types.DATE);
76+
assertThat(((Date[]) array.getArray(1, 1))[0])
77+
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
78+
try (ResultSet rs = array.getResultSet()) {
79+
assertThat(rs.next()).isTrue();
80+
assertThat(rs.getDate(2))
81+
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2021, 1, 18)));
82+
assertThat(rs.next()).isTrue();
83+
assertThat(rs.getDate(2))
84+
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2000, 2, 29)));
85+
assertThat(rs.next()).isTrue();
86+
assertThat(rs.getDate(2))
87+
.isEqualTo(toSqlDate(com.google.cloud.Date.fromYearMonthDay(2019, 8, 31)));
88+
assertThat(rs.next()).isFalse();
89+
}
4990

5091
array = JdbcArray.createArray("FLOAT64", new Double[] {1.1D, 2.2D, Math.PI});
51-
assertEquals(array.getBaseType(), Types.DOUBLE);
52-
assertEquals(((Double[]) array.getArray(1, 3))[2], Double.valueOf(Math.PI));
92+
assertThat(array.getBaseType()).isEqualTo(Types.DOUBLE);
93+
assertThat(((Double[]) array.getArray(1, 3))[2]).isEqualTo(Double.valueOf(Math.PI));
94+
try (ResultSet rs = array.getResultSet()) {
95+
assertThat(rs.next()).isTrue();
96+
assertThat(rs.getDouble(2)).isEqualTo(1.1D);
97+
assertThat(rs.next()).isTrue();
98+
assertThat(rs.getDouble(2)).isEqualTo(2.2D);
99+
assertThat(rs.next()).isTrue();
100+
assertThat(rs.getDouble(2)).isEqualTo(Math.PI);
101+
assertThat(rs.next()).isFalse();
102+
}
53103

54104
array = JdbcArray.createArray("INT64", new Long[] {1L, 2L, 3L});
55-
assertEquals(array.getBaseType(), Types.BIGINT);
56-
assertEquals(((Long[]) array.getArray(1, 1))[0], Long.valueOf(1L));
105+
assertThat(array.getBaseType()).isEqualTo(Types.BIGINT);
106+
assertThat(((Long[]) array.getArray(1, 1))[0]).isEqualTo(Long.valueOf(1L));
107+
try (ResultSet rs = array.getResultSet()) {
108+
assertThat(rs.next()).isTrue();
109+
assertThat(rs.getLong(2)).isEqualTo(1L);
110+
assertThat(rs.next()).isTrue();
111+
assertThat(rs.getLong(2)).isEqualTo(2L);
112+
assertThat(rs.next()).isTrue();
113+
assertThat(rs.getLong(2)).isEqualTo(3L);
114+
assertThat(rs.next()).isFalse();
115+
}
57116

58117
array =
59118
JdbcArray.createArray("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN});
60-
assertEquals(array.getBaseType(), Types.NUMERIC);
61-
assertEquals(((BigDecimal[]) array.getArray(1, 1))[0], BigDecimal.ONE);
62-
assertEquals(((BigDecimal[]) array.getArray(2, 1))[0], null);
63-
assertEquals(((BigDecimal[]) array.getArray(3, 1))[0], BigDecimal.TEN);
119+
assertThat(array.getBaseType()).isEqualTo(Types.NUMERIC);
120+
assertThat(((BigDecimal[]) array.getArray(1, 1))[0]).isEqualTo(BigDecimal.ONE);
121+
assertThat(((BigDecimal[]) array.getArray(2, 1))[0]).isNull();
122+
assertThat(((BigDecimal[]) array.getArray(3, 1))[0]).isEqualTo(BigDecimal.TEN);
123+
try (ResultSet rs = array.getResultSet()) {
124+
assertThat(rs.next()).isTrue();
125+
assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.ONE);
126+
assertThat(rs.next()).isTrue();
127+
assertThat(rs.getBigDecimal(2)).isNull();
128+
assertThat(rs.next()).isTrue();
129+
assertThat(rs.getBigDecimal(2)).isEqualTo(BigDecimal.TEN);
130+
assertThat(rs.next()).isFalse();
131+
}
64132

65133
array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
66-
assertEquals(array.getBaseType(), Types.NVARCHAR);
67-
assertEquals(((String[]) array.getArray(1, 1))[0], "foo");
134+
assertThat(array.getBaseType()).isEqualTo(Types.NVARCHAR);
135+
assertThat(((String[]) array.getArray(1, 1))[0]).isEqualTo("foo");
136+
try (ResultSet rs = array.getResultSet()) {
137+
assertThat(rs.next()).isTrue();
138+
assertThat(rs.getString(2)).isEqualTo("foo");
139+
assertThat(rs.next()).isTrue();
140+
assertThat(rs.getString(2)).isEqualTo("bar");
141+
assertThat(rs.next()).isTrue();
142+
assertThat(rs.getString(2)).isEqualTo("baz");
143+
assertThat(rs.next()).isFalse();
144+
}
68145

69146
array =
70147
JdbcArray.createArray(
71148
"TIMESTAMP",
72149
new Timestamp[] {new Timestamp(1L), new Timestamp(100L), new Timestamp(1000L)});
73-
assertEquals(array.getBaseType(), Types.TIMESTAMP);
74-
assertEquals(((Timestamp[]) array.getArray(1, 1))[0], new Timestamp(1L));
150+
assertThat(array.getBaseType()).isEqualTo(Types.TIMESTAMP);
151+
assertThat(((Timestamp[]) array.getArray(1, 1))[0]).isEqualTo(new Timestamp(1L));
152+
try (ResultSet rs = array.getResultSet()) {
153+
assertThat(rs.next()).isTrue();
154+
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1L));
155+
assertThat(rs.next()).isTrue();
156+
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(100L));
157+
assertThat(rs.next()).isTrue();
158+
assertThat(rs.getTimestamp(2)).isEqualTo(new Timestamp(1000L));
159+
assertThat(rs.next()).isFalse();
160+
}
161+
}
162+
163+
@Test
164+
public void testCreateArrayOfArray() {
165+
try {
166+
JdbcArray.createArray("ARRAY<STRING>", new String[][] {{}});
167+
fail("missing expected exception");
168+
} catch (SQLException e) {
169+
assertThat((Exception) e).isInstanceOf(JdbcSqlException.class);
170+
JdbcSqlException jse = (JdbcSqlException) e;
171+
assertThat(jse.getErrorCode())
172+
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
173+
}
174+
}
175+
176+
@Test
177+
public void testCreateArrayOfStruct() {
178+
try {
179+
JdbcArray.createArray("STRUCT", new Object[] {});
180+
fail("missing expected exception");
181+
} catch (SQLException e) {
182+
assertThat((Exception) e).isInstanceOf(JdbcSqlException.class);
183+
JdbcSqlException jse = (JdbcSqlException) e;
184+
assertThat(jse.getErrorCode())
185+
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
186+
}
187+
}
188+
189+
@Test
190+
public void testGetResultSetMetadata() throws SQLException {
191+
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
192+
try (ResultSet rs = array.getResultSet()) {
193+
ResultSetMetaData metadata = rs.getMetaData();
194+
assertThat(metadata.getColumnCount()).isEqualTo(2);
195+
assertThat(metadata.getColumnType(1)).isEqualTo(Types.BIGINT);
196+
assertThat(metadata.getColumnType(2)).isEqualTo(Types.NVARCHAR);
197+
assertThat(metadata.getColumnName(1)).isEqualTo("INDEX");
198+
assertThat(metadata.getColumnName(2)).isEqualTo("VALUE");
199+
}
200+
}
201+
202+
@Test
203+
public void testGetResultSetWithIndex() throws SQLException {
204+
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
205+
try (ResultSet rs = array.getResultSet(2L, 1)) {
206+
assertThat(rs.next()).isTrue();
207+
assertThat(rs.getLong("INDEX")).isEqualTo(2L);
208+
assertThat(rs.getString("VALUE")).isEqualTo("bar");
209+
assertThat(rs.next()).isFalse();
210+
}
211+
212+
try (ResultSet rs = array.getResultSet(1L, 5)) {
213+
assertThat(rs.next()).isTrue();
214+
assertThat(rs.getString(2)).isEqualTo("foo");
215+
assertThat(rs.next()).isTrue();
216+
assertThat(rs.getString(2)).isEqualTo("bar");
217+
assertThat(rs.next()).isTrue();
218+
assertThat(rs.getString(2)).isEqualTo("baz");
219+
assertThat(rs.next()).isFalse();
220+
}
221+
222+
try (ResultSet rs = array.getResultSet(1L, 0)) {
223+
assertThat(rs.next()).isFalse();
224+
}
225+
}
226+
227+
@Test
228+
public void testGetResultSetWithInvalidIndex() throws SQLException {
229+
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
230+
try (ResultSet rs = array.getResultSet(0L, 1)) {
231+
fail("missing expected exception");
232+
} catch (JdbcSqlExceptionImpl e) {
233+
assertThat(e.getErrorCode())
234+
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
235+
}
236+
}
237+
238+
@Test
239+
public void testGetResultSetWithInvalidCount() throws SQLException {
240+
JdbcArray array = JdbcArray.createArray("STRING", new String[] {"foo", "bar", "baz"});
241+
try (ResultSet rs = array.getResultSet(1L, -1)) {
242+
fail("missing expected exception");
243+
} catch (JdbcSqlExceptionImpl e) {
244+
assertThat(e.getErrorCode())
245+
.isEqualTo(ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value());
246+
}
75247
}
76248
}

0 commit comments

Comments
 (0)