fix(spanner): handle array values and intrinsic numeric DDL
Closes #6841
This commit is contained in:
1 parent
ab0ff28fe4
commit
02402de53f
2 files changed
+197
-3
No files matched your search
@@ -24,9 +24,12 @@ import com.google.cloud.spanner.DatabaseId;
|
||||
import com.google.cloud.spanner.Dialect;
|
||||
import com.google.cloud.spanner.jdbc.CloudSpannerJdbcConnection;
|
||||
|
||||
import java.sql.Array;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -295,6 +298,37 @@ public final class SpannerAgent extends ConfiguredJdbcAgent {
|
||||
return spannerColumns(requireConnection(), resolveSchema(schema), table);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object resultValue(ResultSet rs, int index, int sqlType) {
|
||||
if (sqlType != Types.ARRAY) {
|
||||
return super.resultValue(rs, index, sqlType);
|
||||
}
|
||||
return unchecked(() -> {
|
||||
Array array = rs.getArray(index);
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object raw = array.getArray();
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
int length = java.lang.reflect.Array.getLength(raw);
|
||||
List<Object> values = new ArrayList<>(length);
|
||||
for (int element = 0; element < length; element++) {
|
||||
values.add(java.lang.reflect.Array.get(raw, element));
|
||||
}
|
||||
return values;
|
||||
} finally {
|
||||
try {
|
||||
array.free();
|
||||
} catch (SQLException ignored) {
|
||||
// The value has already been read; cleanup failure must not discard it.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IndexInfo> listIndexes(String schema, String table) {
|
||||
return spannerIndexes(requireConnection(), resolveSchema(schema), table);
|
||||
@@ -485,6 +519,8 @@ public final class SpannerAgent extends ConfiguredJdbcAgent {
|
||||
String name = rs.getString("COLUMN_NAME");
|
||||
// TYPE_NAME already carries the native Spanner type (STRING(100), ARRAY<INT64>).
|
||||
String typeName = rs.getString("TYPE_NAME");
|
||||
Integer columnSize = intOrNull(rs, "COLUMN_SIZE");
|
||||
boolean intrinsicNumeric = isIntrinsicNumericType(typeName);
|
||||
result.add(new ColumnInfo(
|
||||
name,
|
||||
typeName,
|
||||
@@ -493,9 +529,9 @@ public final class SpannerAgent extends ConfiguredJdbcAgent {
|
||||
primaryKeys.contains(name),
|
||||
null,
|
||||
rs.getString("REMARKS"),
|
||||
intOrNull(rs, "COLUMN_SIZE"),
|
||||
intOrNull(rs, "DECIMAL_DIGITS"),
|
||||
characterLength(typeName, intOrNull(rs, "COLUMN_SIZE"))
|
||||
intrinsicNumeric ? null : columnSize,
|
||||
intrinsicNumeric ? null : intOrNull(rs, "DECIMAL_DIGITS"),
|
||||
characterLength(typeName, columnSize)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -726,6 +762,10 @@ public final class SpannerAgent extends ConfiguredJdbcAgent {
|
||||
return columnSize;
|
||||
}
|
||||
|
||||
private static boolean isIntrinsicNumericType(String typeName) {
|
||||
return "NUMERIC".equalsIgnoreCase(typeName) || "DECIMAL".equalsIgnoreCase(typeName);
|
||||
}
|
||||
|
||||
private static Integer intOrNull(ResultSet rs, String column) throws Exception {
|
||||
Object value = rs.getObject(column);
|
||||
return value instanceof Number ? ((Number) value).intValue() : null;
|
||||
|
||||
@@ -13,11 +13,15 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.sql.Array;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DatabaseMetaData;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -263,6 +267,107 @@ class SpannerAgentTest extends JdbcFakeExecutionBehaviorTest {
|
||||
assertTrue(columns.get(1).getIs_nullable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsArrayValuesInOrderWithoutUsingGetStringAndFreesTheArray() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
Array array = fakeArray(new Object[]{7L, "two", null}, calls, false);
|
||||
ResultSet resultSet = arrayResultSet(array, calls);
|
||||
|
||||
Object value = resultValue(new SpannerAgent(), resultSet, Types.ARRAY);
|
||||
|
||||
assertEquals(Arrays.asList(7L, "two", null), value);
|
||||
assertEquals(Arrays.asList("getArray(1)", "array.getArray()", "array.free()"), calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNullForNullArraysWithoutUsingGetString() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
ResultSet resultSet = arrayResultSet(null, calls);
|
||||
|
||||
assertNull(resultValue(new SpannerAgent(), resultSet, Types.ARRAY));
|
||||
assertEquals(Collections.singletonList("getArray(1)"), calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsTheReadArrayValueWhenFreeFails() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
Array array = fakeArray(new String[]{"first", "second"}, calls, true);
|
||||
|
||||
Object value = resultValue(new SpannerAgent(), arrayResultSet(array, calls), Types.ARRAY);
|
||||
|
||||
assertEquals(Arrays.asList("first", "second"), value);
|
||||
assertEquals(Arrays.asList("getArray(1)", "array.getArray()", "array.free()"), calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void delegatesScalarValuesToTheSharedReader() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
ResultSet resultSet = proxy(ResultSet.class, (method, args) -> {
|
||||
if ("getInt".equals(method.getName())) {
|
||||
calls.add("getInt(" + args[0] + ")");
|
||||
return 42;
|
||||
}
|
||||
if ("wasNull".equals(method.getName())) {
|
||||
calls.add("wasNull()");
|
||||
return false;
|
||||
}
|
||||
if ("getArray".equals(method.getName()) || "getString".equals(method.getName())) {
|
||||
throw new AssertionError("Unexpected " + method.getName());
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
|
||||
assertEquals(42, resultValue(new SpannerAgent(), resultSet, Types.INTEGER));
|
||||
assertEquals(Arrays.asList("getInt(1)", "wasNull()"), calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
void omitsIntrinsicNumericParametersWhileKeepingNativeArrayAndStringTypes() {
|
||||
Map<String, List<Map<String, Object>>> rows = new LinkedHashMap<>();
|
||||
rows.put("getColumns", Arrays.asList(
|
||||
row(
|
||||
"COLUMN_NAME", "amount",
|
||||
"TYPE_NAME", "NUMERIC",
|
||||
"NULLABLE", DatabaseMetaData.columnNullable,
|
||||
"COLUMN_SIZE", 15,
|
||||
"DECIMAL_DIGITS", 0
|
||||
),
|
||||
row(
|
||||
"COLUMN_NAME", "ratio",
|
||||
"TYPE_NAME", "DECIMAL",
|
||||
"NULLABLE", DatabaseMetaData.columnNullable,
|
||||
"COLUMN_SIZE", 15,
|
||||
"DECIMAL_DIGITS", 0
|
||||
),
|
||||
row(
|
||||
"COLUMN_NAME", "tags",
|
||||
"TYPE_NAME", "ARRAY<STRING(MAX)>",
|
||||
"NULLABLE", DatabaseMetaData.columnNullable
|
||||
),
|
||||
row(
|
||||
"COLUMN_NAME", "nickname",
|
||||
"TYPE_NAME", "STRING(100)",
|
||||
"NULLABLE", DatabaseMetaData.columnNullable,
|
||||
"COLUMN_SIZE", 100
|
||||
)
|
||||
));
|
||||
SpannerAgent agent = connectedAgent(GOOGLE_SQL_PRODUCT, null, rows);
|
||||
|
||||
List<ColumnInfo> columns = agent.getColumns("", "singers");
|
||||
String ddl = agent.getTableDdl("", "singers");
|
||||
|
||||
assertNull(columns.get(0).getNumeric_precision());
|
||||
assertNull(columns.get(0).getNumeric_scale());
|
||||
assertNull(columns.get(1).getNumeric_precision());
|
||||
assertNull(columns.get(1).getNumeric_scale());
|
||||
assertTrue(ddl.contains("`amount` NUMERIC"), ddl);
|
||||
assertTrue(ddl.contains("`ratio` DECIMAL"), ddl);
|
||||
assertFalse(ddl.contains("NUMERIC("), ddl);
|
||||
assertFalse(ddl.contains("DECIMAL("), ddl);
|
||||
assertTrue(ddl.contains("`tags` ARRAY<STRING(MAX)>"), ddl);
|
||||
assertTrue(ddl.contains("`nickname` STRING(100)"), ddl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsTheConfiguredDatabaseOnlyOnce() {
|
||||
SpannerAgent agent = connectedAgent(POSTGRES_PRODUCT, "public", new LinkedHashMap<>());
|
||||
@@ -566,6 +671,55 @@ class SpannerAgentTest extends JdbcFakeExecutionBehaviorTest {
|
||||
});
|
||||
}
|
||||
|
||||
private static ResultSet arrayResultSet(Array array, List<String> calls) {
|
||||
return proxy(ResultSet.class, (method, args) -> {
|
||||
if ("getArray".equals(method.getName())) {
|
||||
calls.add("getArray(" + args[0] + ")");
|
||||
return array;
|
||||
}
|
||||
if ("getString".equals(method.getName())) {
|
||||
throw new AssertionError("ARRAY values must not be read with getString");
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static Array fakeArray(Object value, List<String> calls, boolean failFree) {
|
||||
return proxy(Array.class, (method, args) -> {
|
||||
if ("getArray".equals(method.getName())) {
|
||||
calls.add("array.getArray()");
|
||||
return value;
|
||||
}
|
||||
if ("free".equals(method.getName())) {
|
||||
calls.add("array.free()");
|
||||
if (failFree) {
|
||||
throw new SQLException("cleanup failed");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
});
|
||||
}
|
||||
|
||||
private static Object resultValue(SpannerAgent agent, ResultSet resultSet, int sqlType) {
|
||||
try {
|
||||
Method method = SpannerAgent.class.getDeclaredMethod("resultValue", ResultSet.class, int.class, int.class);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(agent, resultSet, 1, sqlType);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
}
|
||||
if (cause instanceof Error) {
|
||||
throw (Error) cause;
|
||||
}
|
||||
throw new IllegalStateException(cause);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> row(Object... keyValues) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (int i = 0; i < keyValues.length; i += 2) {
|
||||
|
||||
Reference in new issue
Block a user