diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java index 9f975245..5f296731 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java @@ -48,6 +48,7 @@ static int extractColumnType(Type type) { if (type.equals(Type.int64())) return Types.BIGINT; if (type.equals(Type.numeric())) return Types.NUMERIC; if (type.equals(Type.string())) return Types.NVARCHAR; + if (type.equals(Type.json())) return Types.NVARCHAR; if (type.equals(Type.timestamp())) return Types.TIMESTAMP; if (type.getCode() == Code.ARRAY) return Types.ARRAY; return Types.OTHER; @@ -106,6 +107,7 @@ static String getClassName(Type type) { if (type == Type.int64()) return Long.class.getName(); if (type == Type.numeric()) return BigDecimal.class.getName(); if (type == Type.string()) return String.class.getName(); + if (type == Type.json()) return String.class.getName(); if (type == Type.timestamp()) return Timestamp.class.getName(); if (type.getCode() == Code.ARRAY) { if (type.getArrayElementType() == Type.bool()) return Boolean[].class.getName(); @@ -115,6 +117,7 @@ static String getClassName(Type type) { if (type.getArrayElementType() == Type.int64()) return Long[].class.getName(); if (type.getArrayElementType() == Type.numeric()) return BigDecimal[].class.getName(); if (type.getArrayElementType() == Type.string()) return String[].class.getName(); + if (type.getArrayElementType() == Type.json()) return String[].class.getName(); if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName(); } return null; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java index 2a9d9e77..43677405 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java @@ -21,6 +21,7 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.Value; import com.google.cloud.spanner.ValueBinder; import com.google.common.collect.ImmutableList; import com.google.rpc.Code; @@ -201,6 +202,9 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException { case STRING: builder = binder.to((String) value); break; + case JSON: + builder = binder.to(Value.json((String) value)); + break; case TIMESTAMP: builder = binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value)); break; diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java index 6e6c3bf8..8da36e4c 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java @@ -229,6 +229,32 @@ public Type getSpannerType() { return Type.string(); } }, + JSON { + @Override + public int getSqlType() { + return JsonType.VENDOR_TYPE_NUMBER; + } + + @Override + public Class getJavaClass() { + return String.class; + } + + @Override + public Code getCode() { + return Code.JSON; + } + + @Override + public List getArrayElements(ResultSet rs, int columnIndex) { + return rs.getJsonList(columnIndex); + } + + @Override + public Type getSpannerType() { + return Type.json(); + } + }, TIMESTAMP { @Override public int getSqlType() { diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java index 75f16cd8..24773cdc 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java @@ -19,6 +19,7 @@ import com.google.cloud.ByteArray; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Statement.Builder; +import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Value; import com.google.cloud.spanner.ValueBinder; import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl; @@ -264,6 +265,7 @@ private boolean isTypeSupported(int sqlType) { case Types.NCLOB: case Types.NUMERIC: case Types.DECIMAL: + case JsonType.VENDOR_TYPE_NUMBER: return true; } return false; @@ -315,6 +317,11 @@ private boolean isValidTypeAndValue(Object value, int sqlType) { return value instanceof Clob || value instanceof Reader; case Types.NCLOB: return value instanceof NClob || value instanceof Reader; + case JsonType.VENDOR_TYPE_NUMBER: + return value instanceof String + || value instanceof InputStream + || value instanceof Reader + || (value instanceof Value && ((Value) value).getType().getCode() == Type.Code.JSON); } return false; } @@ -544,30 +551,34 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value, case Types.NCHAR: case Types.NVARCHAR: case Types.LONGNVARCHAR: + String stringValue; if (value instanceof String) { - return binder.to((String) value); + stringValue = (String) value; } else if (value instanceof InputStream) { - InputStreamReader reader = - new InputStreamReader((InputStream) value, StandardCharsets.US_ASCII); - try { - return binder.to(CharStreams.toString(reader)); - } catch (IOException e) { - throw JdbcSqlExceptionFactory.of( - "could not set string from input stream", Code.INVALID_ARGUMENT, e); - } + stringValue = getStringFromInputStream((InputStream) value); } else if (value instanceof Reader) { - try { - return binder.to(CharStreams.toString((Reader) value)); - } catch (IOException e) { - throw JdbcSqlExceptionFactory.of( - "could not set string from reader", Code.INVALID_ARGUMENT, e); - } + stringValue = getStringFromReader((Reader) value); } else if (value instanceof URL) { - return binder.to(((URL) value).toString()); + stringValue = ((URL) value).toString(); } else if (value instanceof UUID) { - return binder.to(((UUID) value).toString()); + stringValue = ((UUID) value).toString(); + } else { + throw JdbcSqlExceptionFactory.of(value + " is not a valid string", Code.INVALID_ARGUMENT); + } + return binder.to(stringValue); + case JsonType.VENDOR_TYPE_NUMBER: + String jsonValue; + if (value instanceof String) { + jsonValue = (String) value; + } else if (value instanceof InputStream) { + jsonValue = getStringFromInputStream((InputStream) value); + } else if (value instanceof Reader) { + jsonValue = getStringFromReader((Reader) value); + } else { + throw JdbcSqlExceptionFactory.of( + value + " is not a valid JSON value", Code.INVALID_ARGUMENT); } - throw JdbcSqlExceptionFactory.of(value + " is not a valid string", Code.INVALID_ARGUMENT); + return binder.to(Value.json(jsonValue)); case Types.DATE: if (value instanceof Date) { return binder.to(JdbcTypeConverter.toGoogleDate((Date) value)); @@ -652,6 +663,25 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value, return null; } + private String getStringFromInputStream(InputStream inputStream) throws SQLException { + InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); + try { + return CharStreams.toString(reader); + } catch (IOException e) { + throw JdbcSqlExceptionFactory.of( + "could not set string from input stream", Code.INVALID_ARGUMENT, e); + } + } + + private String getStringFromReader(Reader reader) throws SQLException { + try { + return CharStreams.toString(reader); + } catch (IOException e) { + throw JdbcSqlExceptionFactory.of( + "could not set string from reader", Code.INVALID_ARGUMENT, e); + } + } + /** Set the parameter value based purely on the type of the value. */ private Builder setParamWithUnknownType(ValueBinder binder, Object value) throws SQLException { @@ -769,14 +799,16 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu case Types.LONGNVARCHAR: case Types.CLOB: case Types.NCLOB: - return binder.toStringArray((Iterable) null); + return binder.toStringArray(null); + case JsonType.VENDOR_TYPE_NUMBER: + return binder.toJsonArray(null); case Types.DATE: - return binder.toDateArray((Iterable) null); + return binder.toDateArray(null); case Types.TIME: case Types.TIME_WITH_TIMEZONE: case Types.TIMESTAMP: case Types.TIMESTAMP_WITH_TIMEZONE: - return binder.toTimestampArray((Iterable) null); + return binder.toTimestampArray(null); case Types.BINARY: case Types.VARBINARY: case Types.LONGVARBINARY: @@ -829,7 +861,11 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu } else if (Timestamp[].class.isAssignableFrom(value.getClass())) { return binder.toTimestampArray(JdbcTypeConverter.toGoogleTimestamps((Timestamp[]) value)); } else if (String[].class.isAssignableFrom(value.getClass())) { - return binder.toStringArray(Arrays.asList((String[]) value)); + if (type == JsonType.VENDOR_TYPE_NUMBER) { + return binder.toJsonArray(Arrays.asList((String[]) value)); + } else { + return binder.toStringArray(Arrays.asList((String[]) value)); + } } else if (byte[][].class.isAssignableFrom(value.getClass())) { return binder.toBytesArray(JdbcTypeConverter.toGoogleBytes((byte[][]) value)); } diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java index c4ca6b5e..489c2d27 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java @@ -142,6 +142,8 @@ public String getString(int columnIndex) throws SQLException { return isNull ? null : spanner.getBigDecimal(spannerIndex).toString(); case STRING: return isNull ? null : spanner.getString(spannerIndex); + case JSON: + return isNull ? null : spanner.getJson(spannerIndex); case TIMESTAMP: return isNull ? null : spanner.getTimestamp(spannerIndex).toString(); case STRUCT: @@ -169,6 +171,7 @@ public boolean getBoolean(int columnIndex) throws SQLException { case STRING: return isNull ? false : Boolean.valueOf(spanner.getString(spannerIndex)); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -198,6 +201,7 @@ public byte getByte(int columnIndex) throws SQLException { case STRING: return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex))); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -227,6 +231,7 @@ public short getShort(int columnIndex) throws SQLException { case STRING: return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex))); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -256,6 +261,7 @@ public int getInt(int columnIndex) throws SQLException { case STRING: return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex))); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -283,6 +289,7 @@ public long getLong(int columnIndex) throws SQLException { case STRING: return isNull ? 0L : parseLong(spanner.getString(spannerIndex)); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -310,6 +317,7 @@ public float getFloat(int columnIndex) throws SQLException { case STRING: return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex))); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -337,6 +345,7 @@ public double getDouble(int columnIndex) throws SQLException { case STRING: return isNull ? 0 : parseDouble(spanner.getString(spannerIndex)); case BYTES: + case JSON: case DATE: case STRUCT: case TIMESTAMP: @@ -372,6 +381,7 @@ public Date getDate(int columnIndex) throws SQLException { case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: @@ -396,6 +406,7 @@ public Time getTime(int columnIndex) throws SQLException { case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: @@ -421,6 +432,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: @@ -576,6 +588,7 @@ private Object getObject(Type type, int columnIndex) throws SQLException { if (type == Type.int64()) return getLong(columnIndex); if (type == Type.numeric()) return getBigDecimal(columnIndex); if (type == Type.string()) return getString(columnIndex); + if (type == Type.json()) return getString(columnIndex); if (type == Type.timestamp()) return getTimestamp(columnIndex); if (type.getCode() == Code.ARRAY) return getArray(columnIndex); throw JdbcSqlExceptionFactory.of( @@ -664,6 +677,7 @@ private BigDecimal getBigDecimal(int columnIndex, boolean fixedScale, int scale) e); } case BYTES: + case JSON: case DATE: case TIMESTAMP: case STRUCT: @@ -749,6 +763,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: @@ -778,6 +793,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: @@ -810,6 +826,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException case INT64: case NUMERIC: case BYTES: + case JSON: case STRUCT: case ARRAY: default: diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java index 6aa9b48a..4b2e4f0f 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java @@ -81,7 +81,8 @@ static Object convert(Object value, Type type, Class targetType) throws SQLEx } if (targetType.equals(byte[].class)) { if (type.getCode() == Code.BYTES) return value; - if (type.getCode() == Code.STRING) return ((String) value).getBytes(UTF8); + if (type.getCode() == Code.STRING || type.getCode() == Code.JSON) + return ((String) value).getBytes(UTF8); } if (targetType.equals(Boolean.class)) { if (type.getCode() == Code.BOOL) return value; @@ -186,6 +187,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx case TIMESTAMP: return Value.timestampArray( toGoogleTimestamps((java.sql.Timestamp[]) ((java.sql.Array) value).getArray())); + case JSON: + return Value.jsonArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray())); case STRUCT: default: throw JdbcSqlExceptionFactory.of( @@ -207,6 +210,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx return Value.string((String) value); case TIMESTAMP: return Value.timestamp(toGoogleTimestamp((java.sql.Timestamp) value)); + case JSON: + return Value.json((String) value); case STRUCT: default: throw JdbcSqlExceptionFactory.of( diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java b/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java new file mode 100644 index 00000000..44af26b6 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/jdbc/JsonType.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.jdbc; + +import com.google.spanner.v1.TypeCode; +import java.sql.PreparedStatement; +import java.sql.SQLType; + +/** + * Custom SQL type for Spanner JSON data type. This type (or the vendor type number) must be used + * when setting a JSON parameter using {@link PreparedStatement#setObject(int, Object, SQLType)}. + */ +public class JsonType implements SQLType { + public static final JsonType INSTANCE = new JsonType(); + /** + * Spanner does not have any type numbers, but the code values are unique. Add 100,000 to avoid + * conflicts with the type numbers in java.sql.Types. + */ + public static final int VENDOR_TYPE_NUMBER = 100_000 + TypeCode.JSON_VALUE; + + private JsonType() {} + + @Override + public String getName() { + return "JSON"; + } + + @Override + public String getVendor() { + return JsonType.class.getPackage().getName(); + } + + @Override + public Integer getVendorTypeNumber() { + return VENDOR_TYPE_NUMBER; + } + + public String toString() { + return getName(); + } +} diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql index 69b1f6a8..14106ee1 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql @@ -24,6 +24,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU WHEN SPANNER_TYPE = 'INT64' THEN -5 WHEN SPANNER_TYPE = 'NUMERIC' THEN 2 WHEN SPANNER_TYPE LIKE 'STRING%' THEN -9 + WHEN SPANNER_TYPE = 'JSON' THEN -9 WHEN SPANNER_TYPE = 'TIMESTAMP' THEN 93 END AS DATA_TYPE, SPANNER_TYPE AS TYPE_NAME, @@ -36,6 +37,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU WHEN SPANNER_TYPE = 'BOOL' OR SPANNER_TYPE = 'ARRAY' THEN NULL WHEN SPANNER_TYPE = 'DATE' OR SPANNER_TYPE = 'ARRAY' THEN 10 WHEN SPANNER_TYPE = 'TIMESTAMP' OR SPANNER_TYPE = 'ARRAY' THEN 35 + WHEN SPANNER_TYPE = 'JSON' OR SPANNER_TYPE = 'ARRAY' THEN 2621440 ELSE 0 END ELSE CAST(REPLACE(SUBSTR(SPANNER_TYPE, STRPOS(SPANNER_TYPE, '(')+1, STRPOS(SPANNER_TYPE, ')')-STRPOS(SPANNER_TYPE, '(')-1), 'MAX', CASE WHEN UPPER(SPANNER_TYPE) LIKE '%STRING%' THEN '2621440' ELSE '10485760' END) AS INT64) @@ -63,6 +65,7 @@ SELECT TABLE_CATALOG AS TABLE_CAT, TABLE_SCHEMA AS TABLE_SCHEM, TABLE_NAME, COLU CASE WHEN (SPANNER_TYPE LIKE 'STRING%' OR SPANNER_TYPE LIKE 'ARRAY getAllTypes() { types.add(Type.float64()); types.add(Type.int64()); types.add(Type.string()); + types.add(Type.json()); types.add(Type.timestamp()); List arrayTypes = new ArrayList<>(); for (Type type : types) { @@ -219,6 +221,7 @@ private Value getDefaultValue(Type type, int row) { if (type == Type.float64()) return Value.float64(123.45D); if (type == Type.int64()) return Value.int64(12345L); if (type == Type.string()) return Value.string("test value " + row); + if (type == Type.json()) return Value.json("{\"test_value\": " + row + "}"); if (type == Type.timestamp()) return Value.timestamp(com.google.cloud.Timestamp.now()); if (type.getCode() == Code.ARRAY) { @@ -240,6 +243,9 @@ private Value getDefaultValue(Type type, int row) { return Value.int64Array(Arrays.asList(12345L, 54321L)); if (type.getArrayElementType() == Type.string()) return Value.stringArray(Arrays.asList("test value " + row, "test value " + row)); + if (type.getArrayElementType() == Type.json()) + return Value.jsonArray( + Arrays.asList("{\"test_value\": " + row + "}", "{\"test_value\": " + row + "}")); if (type.getArrayElementType() == Type.timestamp()) return Value.timestampArray( Arrays.asList(com.google.cloud.Timestamp.now(), com.google.cloud.Timestamp.now())); @@ -263,7 +269,9 @@ public void testIsAutoIncrement() throws SQLException { public void testIsCaseSensitive() throws SQLException { for (int i = 1; i <= TEST_COLUMNS.size(); i++) { Type type = TEST_COLUMNS.get(i - 1).type; - assertEquals(type == Type.string() || type == Type.bytes(), subject.isCaseSensitive(i)); + assertEquals( + type == Type.string() || type == Type.bytes() || type == Type.json(), + subject.isCaseSensitive(i)); } } @@ -319,6 +327,7 @@ private int getDefaultDisplaySize(Type type, int column) throws SQLException { int length = subject.getPrecision(column); return length == 0 ? 50 : length; } + if (type == Type.json()) return 50; if (type == Type.timestamp()) return 16; return 10; } @@ -397,6 +406,7 @@ private int getSqlType(Type type) { if (type == Type.float64()) return Types.DOUBLE; if (type == Type.int64()) return Types.BIGINT; if (type == Type.string()) return Types.NVARCHAR; + if (type == Type.json()) return Types.NVARCHAR; if (type == Type.timestamp()) return Types.TIMESTAMP; if (type.getCode() == Code.ARRAY) return Types.ARRAY; return Types.OTHER; @@ -446,6 +456,7 @@ private String getTypeClassName(Type type) { if (type == Type.float64()) return Double.class.getName(); if (type == Type.int64()) return Long.class.getName(); if (type == Type.string()) return String.class.getName(); + if (type == Type.json()) return String.class.getName(); if (type == Type.timestamp()) return Timestamp.class.getName(); if (type.getCode() == Code.ARRAY) { if (type.getArrayElementType() == Type.bool()) return Boolean[].class.getName(); @@ -454,6 +465,7 @@ private String getTypeClassName(Type type) { if (type.getArrayElementType() == Type.float64()) return Double[].class.getName(); if (type.getArrayElementType() == Type.int64()) return Long[].class.getName(); if (type.getArrayElementType() == Type.string()) return String[].class.getName(); + if (type.getArrayElementType() == Type.json()) return String[].class.getName(); if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName(); } return null; @@ -466,15 +478,17 @@ private String getTypeClassName(Type type) { + "Col 4: COL4 FLOAT64\n" + "Col 5: COL5 INT64\n" + "Col 6: COL6 STRING\n" - + "Col 7: COL7 TIMESTAMP\n" - + "Col 8: COL8 ARRAY\n" + + "Col 7: COL7 JSON\n" + + "Col 8: COL8 TIMESTAMP\n" + "Col 9: COL9 ARRAY\n" + "Col 10: COL10 ARRAY\n" + "Col 11: COL11 ARRAY\n" + "Col 12: COL12 ARRAY\n" + "Col 13: COL13 ARRAY\n" + "Col 14: COL14 ARRAY\n" - + "Col 15: CALCULATED INT64\n"; + + "Col 15: COL15 ARRAY\n" + + "Col 16: COL16 ARRAY\n" + + "Col 17: CALCULATED INT64\n"; @Test public void testToString() { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java index 9fa5f3e9..506992c4 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java @@ -132,6 +132,11 @@ public class JdbcResultSetTest { private static final BigDecimal NUMERIC_VALUE = new BigDecimal("3.14"); private static final int NUMERIC_COLINDEX_NULL = 25; private static final int NUMERIC_COLINDEX_NOTNULL = 26; + private static final String JSON_COL_NULL = "JSON_COL_NULL"; + private static final String JSON_COL_NOT_NULL = "JSON_COL_NOT_NULL"; + private static final int JSON_COLINDEX_NULL = 27; + private static final int JSON_COLINDEX_NOT_NULL = 28; + private static final String JSON_VALUE = "{\"name\":\"John\", \"age\":30, \"car\":null}"; private static final String BOOL_ARRAY_COL = "BOOL_ARRAY"; private static final List BOOL_ARRAY_VALUE = Arrays.asList(true, null, false); @@ -149,6 +154,8 @@ public class JdbcResultSetTest { private static final List STRING_ARRAY_VALUE = Arrays.asList(STRING_VALUE, null); private static final String TIMESTAMP_ARRAY_COL = "TIMESTAMP_ARRAY"; private static final List TIMESTAMP_ARRAY_VALUE = Arrays.asList(TIMESTAMP_VALUE, null); + private static final String JSON_ARRAY_COL = "JSON_ARRAY"; + private static final List JSON_ARRAY_VALUE = Arrays.asList(JSON_VALUE, null); private JdbcResultSet subject; @@ -181,12 +188,15 @@ static ResultSet getMockResultSet() { StructField.of(STRING_COL_TIME, Type.string()), StructField.of(NUMERIC_COL_NULL, Type.numeric()), StructField.of(NUMERIC_COL_NOT_NULL, Type.numeric()), + StructField.of(JSON_COL_NULL, Type.json()), + StructField.of(JSON_COL_NOT_NULL, Type.json()), StructField.of(BOOL_ARRAY_COL, Type.array(Type.bool())), StructField.of(BYTES_ARRAY_COL, Type.array(Type.bytes())), StructField.of(DATE_ARRAY_COL, Type.array(Type.date())), StructField.of(FLOAT64_ARRAY_COL, Type.array(Type.float64())), StructField.of(INT64_ARRAY_COL, Type.array(Type.int64())), StructField.of(NUMERIC_ARRAY_COL, Type.array(Type.numeric())), + StructField.of(JSON_ARRAY_COL, Type.array(Type.json())), StructField.of(STRING_ARRAY_COL, Type.array(Type.string())), StructField.of(TIMESTAMP_ARRAY_COL, Type.array(Type.timestamp()))), Arrays.asList( @@ -243,6 +253,10 @@ static ResultSet getMockResultSet() { .to((BigDecimal) null) .set(NUMERIC_COL_NOT_NULL) .to(NUMERIC_VALUE) + .set(JSON_COL_NULL) + .to(Value.json(null)) + .set(JSON_COL_NOT_NULL) + .to(Value.json(JSON_VALUE)) .set(BOOL_ARRAY_COL) .toBoolArray(BOOL_ARRAY_VALUE) .set(BYTES_ARRAY_COL) @@ -255,6 +269,8 @@ static ResultSet getMockResultSet() { .toInt64Array(INT64_ARRAY_VALUE) .set(NUMERIC_ARRAY_COL) .toNumericArray(NUMERIC_ARRAY_VALUE) + .set(JSON_ARRAY_COL) + .toJsonArray(JSON_ARRAY_VALUE) .set(STRING_ARRAY_COL) .toStringArray(STRING_ARRAY_VALUE) .set(TIMESTAMP_ARRAY_COL) @@ -322,6 +338,15 @@ public void testGetStringIndex() throws SQLException { assertTrue(subject.wasNull()); } + @Test + public void testGetJsonIndex() throws SQLException { + assertNotNull(subject.getString(JSON_COLINDEX_NOT_NULL)); + assertEquals(JSON_VALUE, subject.getString(JSON_COLINDEX_NOT_NULL)); + assertFalse(subject.wasNull()); + assertNull(subject.getString(JSON_COLINDEX_NULL)); + assertTrue(subject.wasNull()); + } + @Test public void testGetStringIndexForBool() throws SQLException { assertNotNull(subject.getString(BOOLEAN_COLINDEX_NOTNULL)); @@ -1344,6 +1369,15 @@ public void testGetObjectIndex() throws SQLException { assertTrue(subject.wasNull()); } + @Test + public void testGetJsonAsObjectIndex() throws SQLException { + assertNotNull(subject.getObject(JSON_COLINDEX_NOT_NULL)); + assertEquals(JSON_VALUE, subject.getObject(JSON_COLINDEX_NOT_NULL)); + assertFalse(subject.wasNull()); + assertNull(subject.getObject(JSON_COLINDEX_NULL)); + assertTrue(subject.wasNull()); + } + @SuppressWarnings("deprecation") @Test public void testGetObjectLabelMap() throws SQLException { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java index 19f760bb..1bb0e374 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTypeConverterTest.java @@ -70,6 +70,7 @@ public void testConvertArray() throws SQLException { Type.float64(), Type.int64(), Type.string(), + Type.json(), Type.timestamp(), Type.numeric() }) { @@ -126,6 +127,23 @@ public void testConvertBytes() throws SQLException { assertThat(convert(testValues, Type.bytes(), String.class)).isEqualTo("test"); } + @Test + public void testConvertJson() throws SQLException { + String testValue = "{\"test\": foo}"; + assertConvertThrows(testValue, Type.json(), Boolean.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Byte.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Short.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Integer.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Long.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Float.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), Double.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), BigInteger.class, Code.INVALID_ARGUMENT); + assertConvertThrows(testValue, Type.json(), BigDecimal.class, Code.INVALID_ARGUMENT); + + assertThat(convert(testValue, Type.json(), byte[].class)).isEqualTo(testValue.getBytes(UTF8)); + assertThat(convert(testValue, Type.json(), String.class)).isEqualTo(testValue); + } + private TimeZone[] getTestTimeZones() { return new TimeZone[] { TimeZone.getTimeZone("GMT-12:00"), diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java index 75553998..087d688f 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcDatabaseMetaDataTest.java @@ -122,6 +122,7 @@ private Column( new Column("ColTimestamp", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null), new Column("ColCommitTS", Types.TIMESTAMP, "TIMESTAMP", 35, null, null, false, null), new Column("ColNumeric", Types.NUMERIC, "NUMERIC", 15, null, 10, false, null), + new Column("ColJson", Types.NVARCHAR, "JSON", 2621440, null, null, false, 2621440), new Column("ColInt64Array", Types.ARRAY, "ARRAY", 19, null, 10, true, null), new Column("ColFloat64Array", Types.ARRAY, "ARRAY", 15, 16, 2, true, null), new Column("ColBoolArray", Types.ARRAY, "ARRAY", null, null, null, true, null), @@ -151,6 +152,8 @@ private Column( new Column( "ColTimestampArray", Types.ARRAY, "ARRAY", 35, null, null, true, null), new Column("ColNumericArray", Types.ARRAY, "ARRAY", 15, null, 10, true, null), + new Column( + "ColJsonArray", Types.ARRAY, "ARRAY", 2621440, null, null, true, 2621440), new Column( "ColComputed", Types.NVARCHAR, diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java index d52e5602..ea1bbfb3 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPreparedStatementTest.java @@ -30,6 +30,8 @@ import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.Value; import com.google.cloud.spanner.jdbc.ITAbstractJdbcTest; +import com.google.cloud.spanner.jdbc.JsonType; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; import com.google.common.base.Strings; import com.google.common.io.BaseEncoding; import java.io.File; @@ -748,84 +750,113 @@ public void test07_StatementBatchUpdateWithException() throws SQLException { @Test public void test08_InsertAllColumnTypes() throws SQLException { + String sql; + if (EmulatorSpannerHelper.isUsingEmulator()) { + sql = + "INSERT INTO TableWithAllColumnTypes (" + + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, " + + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } else { + sql = + "INSERT INTO TableWithAllColumnTypes (" + + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, ColJson, " + + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } try (Connection con = createConnection()) { - try (PreparedStatement ps = - con.prepareStatement( - "INSERT INTO TableWithAllColumnTypes (" - + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, " - + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray" - + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { - ps.setLong(1, 1L); - ps.setDouble(2, 2D); - ps.setBoolean(3, true); - ps.setString(4, "test"); - ps.setObject(5, UUID.fromString("2d37f522-e0a5-4f22-8e09-4d77d299c967")); - ps.setBytes(6, "test".getBytes()); - ps.setBytes(7, "testtest".getBytes()); - ps.setDate(8, new Date(System.currentTimeMillis())); - ps.setTimestamp(9, new Timestamp(System.currentTimeMillis())); - ps.setBigDecimal(10, BigDecimal.TEN); - ps.setArray(11, con.createArrayOf("INT64", new Long[] {1L, 2L, 3L})); - ps.setArray(12, con.createArrayOf("FLOAT64", new Double[] {1.1D, 2.2D, 3.3D})); + try (PreparedStatement ps = con.prepareStatement(sql)) { + int index = 0; + ps.setLong(++index, 1L); + ps.setDouble(++index, 2D); + ps.setBoolean(++index, true); + ps.setString(++index, "test"); + ps.setObject(++index, UUID.fromString("2d37f522-e0a5-4f22-8e09-4d77d299c967")); + ps.setBytes(++index, "test".getBytes()); + ps.setBytes(++index, "testtest".getBytes()); + ps.setDate(++index, new Date(System.currentTimeMillis())); + ps.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); + ps.setBigDecimal(++index, BigDecimal.TEN); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + ps.setObject(++index, "{\"test_value\": \"foo\"}", JsonType.INSTANCE); + } + ps.setArray(++index, con.createArrayOf("INT64", new Long[] {1L, 2L, 3L})); + ps.setArray(++index, con.createArrayOf("FLOAT64", new Double[] {1.1D, 2.2D, 3.3D})); ps.setArray( - 13, con.createArrayOf("BOOL", new Boolean[] {Boolean.TRUE, null, Boolean.FALSE})); - ps.setArray(14, con.createArrayOf("STRING", new String[] {"1", "2", "3"})); - ps.setArray(15, con.createArrayOf("STRING", new String[] {"3", "2", "1"})); + ++index, con.createArrayOf("BOOL", new Boolean[] {Boolean.TRUE, null, Boolean.FALSE})); + ps.setArray(++index, con.createArrayOf("STRING", new String[] {"1", "2", "3"})); + ps.setArray(++index, con.createArrayOf("STRING", new String[] {"3", "2", "1"})); ps.setArray( - 16, + ++index, con.createArrayOf( "BYTES", new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()})); ps.setArray( - 17, + ++index, con.createArrayOf( "BYTES", new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()})); ps.setArray( - 18, + ++index, con.createArrayOf( "DATE", new Date[] {new Date(System.currentTimeMillis()), null, new Date(0)})); ps.setArray( - 19, + ++index, con.createArrayOf( "TIMESTAMP", new Timestamp[] { new Timestamp(System.currentTimeMillis()), null, new Timestamp(0) })); ps.setArray( - 20, + ++index, con.createArrayOf("NUMERIC", new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN})); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + ps.setArray( + ++index, + con.createArrayOf( + "JSON", new String[] {"{\"test_value\": \"foo\"}", "{}", "[]", null})); + } assertEquals(1, ps.executeUpdate()); } try (ResultSet rs = con.createStatement().executeQuery("SELECT * FROM TableWithAllColumnTypes")) { + int index = 0; assertTrue(rs.next()); - assertEquals(1L, rs.getLong(1)); - assertEquals(2d, rs.getDouble(2), 0.0d); - assertTrue(rs.getBoolean(3)); - assertEquals("test", rs.getString(4)); - assertEquals("2d37f522-e0a5-4f22-8e09-4d77d299c967", rs.getString(5)); - assertArrayEquals("test".getBytes(), rs.getBytes(6)); - assertArrayEquals("testtest".getBytes(), rs.getBytes(7)); - assertNotNull(rs.getDate(8)); - assertNotNull(rs.getTimestamp(9)); - assertNotNull(rs.getTime(10)); // Commit timestamp - assertEquals(BigDecimal.TEN, rs.getBigDecimal(11)); - assertArrayEquals(new Long[] {1L, 2L, 3L}, (Long[]) rs.getArray(12).getArray()); - assertArrayEquals(new Double[] {1.1D, 2.2D, 3.3D}, (Double[]) rs.getArray(13).getArray()); + assertEquals(1L, rs.getLong(++index)); + assertEquals(2d, rs.getDouble(++index), 0.0d); + assertTrue(rs.getBoolean(++index)); + assertEquals("test", rs.getString(++index)); + assertEquals("2d37f522-e0a5-4f22-8e09-4d77d299c967", rs.getString(++index)); + assertArrayEquals("test".getBytes(), rs.getBytes(++index)); + assertArrayEquals("testtest".getBytes(), rs.getBytes(++index)); + assertNotNull(rs.getDate(++index)); + assertNotNull(rs.getTimestamp(++index)); + assertNotNull(rs.getTime(++index)); // Commit timestamp + assertEquals(BigDecimal.TEN, rs.getBigDecimal(++index)); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + assertEquals("{\"test_value\":\"foo\"}", rs.getString(++index)); + } + assertArrayEquals(new Long[] {1L, 2L, 3L}, (Long[]) rs.getArray(++index).getArray()); + assertArrayEquals( + new Double[] {1.1D, 2.2D, 3.3D}, (Double[]) rs.getArray(++index).getArray()); assertArrayEquals( - new Boolean[] {true, null, false}, (Boolean[]) rs.getArray(14).getArray()); - assertArrayEquals(new String[] {"1", "2", "3"}, (String[]) rs.getArray(15).getArray()); - assertArrayEquals(new String[] {"3", "2", "1"}, (String[]) rs.getArray(16).getArray()); + new Boolean[] {true, null, false}, (Boolean[]) rs.getArray(++index).getArray()); + assertArrayEquals(new String[] {"1", "2", "3"}, (String[]) rs.getArray(++index).getArray()); + assertArrayEquals(new String[] {"3", "2", "1"}, (String[]) rs.getArray(++index).getArray()); assertArrayEquals( new byte[][] {"1".getBytes(), "2".getBytes(), "3".getBytes()}, - (byte[][]) rs.getArray(17).getArray()); + (byte[][]) rs.getArray(++index).getArray()); assertArrayEquals( new byte[][] {"333".getBytes(), "222".getBytes(), "111".getBytes()}, - (byte[][]) rs.getArray(18).getArray()); - assertEquals(3, ((Date[]) rs.getArray(19).getArray()).length); - assertEquals(3, ((Timestamp[]) rs.getArray(20).getArray()).length); + (byte[][]) rs.getArray(++index).getArray()); + assertEquals(3, ((Date[]) rs.getArray(++index).getArray()).length); + assertEquals(3, ((Timestamp[]) rs.getArray(++index).getArray()).length); assertArrayEquals( new BigDecimal[] {BigDecimal.ONE, null, BigDecimal.TEN}, - (BigDecimal[]) rs.getArray(21).getArray()); + (BigDecimal[]) rs.getArray(++index).getArray()); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + assertArrayEquals( + new String[] {"{\"test_value\":\"foo\"}", "{}", "[]", null}, + (String[]) rs.getArray(++index).getArray()); + } assertFalse(rs.next()); } } @@ -838,7 +869,7 @@ public void test09_MetaData_FromQuery() throws SQLException { try (PreparedStatement ps = con.prepareStatement("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=?")) { ResultSetMetaData metadata = ps.getMetaData(); - assertEquals(22, metadata.getColumnCount()); + assertEquals(24, metadata.getColumnCount()); int index = 0; assertEquals("ColInt64", metadata.getColumnLabel(++index)); assertEquals("ColFloat64", metadata.getColumnLabel(++index)); @@ -851,6 +882,7 @@ public void test09_MetaData_FromQuery() throws SQLException { assertEquals("ColTimestamp", metadata.getColumnLabel(++index)); assertEquals("ColCommitTS", metadata.getColumnLabel(++index)); assertEquals("ColNumeric", metadata.getColumnLabel(++index)); + assertEquals("ColJson", metadata.getColumnLabel(++index)); assertEquals("ColInt64Array", metadata.getColumnLabel(++index)); assertEquals("ColFloat64Array", metadata.getColumnLabel(++index)); assertEquals("ColBoolArray", metadata.getColumnLabel(++index)); @@ -861,6 +893,7 @@ public void test09_MetaData_FromQuery() throws SQLException { assertEquals("ColDateArray", metadata.getColumnLabel(++index)); assertEquals("ColTimestampArray", metadata.getColumnLabel(++index)); assertEquals("ColNumericArray", metadata.getColumnLabel(++index)); + assertEquals("ColJsonArray", metadata.getColumnLabel(++index)); assertEquals("ColComputed", metadata.getColumnLabel(++index)); } } @@ -880,104 +913,135 @@ public void test10_MetaData_FromDML() throws SQLException { @Test public void test11_InsertDataUsingSpannerValue() throws SQLException { + String sql; + if (EmulatorSpannerHelper.isUsingEmulator()) { + sql = + "INSERT INTO TableWithAllColumnTypes (" + + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, " + + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } else { + sql = + "INSERT INTO TableWithAllColumnTypes (" + + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, ColJson, " + + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray, ColJsonArray" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } try (Connection con = createConnection()) { - try (PreparedStatement ps = - con.prepareStatement( - "INSERT INTO TableWithAllColumnTypes (" - + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColBytesMax, ColDate, ColTimestamp, ColCommitTS, ColNumeric, " - + "ColInt64Array, ColFloat64Array, ColBoolArray, ColStringArray, ColStringMaxArray, ColBytesArray, ColBytesMaxArray, ColDateArray, ColTimestampArray, ColNumericArray" - + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, PENDING_COMMIT_TIMESTAMP(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { - ps.setObject(1, Value.int64(2L)); - ps.setObject(2, Value.float64(2D)); - ps.setObject(3, Value.bool(true)); - ps.setObject(4, Value.string("testvalues")); - ps.setObject(5, Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967")); - ps.setObject(6, Value.bytes(ByteArray.copyFrom("test".getBytes()))); - ps.setObject(7, Value.bytes(ByteArray.copyFrom("testtest".getBytes()))); - ps.setObject(8, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))); + try (PreparedStatement ps = con.prepareStatement(sql)) { + int index = 1; + ps.setObject(index++, Value.int64(2L)); + ps.setObject(index++, Value.float64(2D)); + ps.setObject(index++, Value.bool(true)); + ps.setObject(index++, Value.string("testvalues")); + ps.setObject(index++, Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967")); + ps.setObject(index++, Value.bytes(ByteArray.copyFrom("test".getBytes()))); + ps.setObject(index++, Value.bytes(ByteArray.copyFrom("testtest".getBytes()))); + ps.setObject(index++, Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3))); ps.setObject( - 9, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99))); - ps.setObject(10, Value.numeric(BigDecimal.TEN)); - ps.setObject(11, Value.int64Array(new long[] {1L, 2L, 3L})); - ps.setObject(12, Value.float64Array(new double[] {1.1D, 2.2D, 3.3D})); - ps.setObject(13, Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE))); - ps.setObject(14, Value.stringArray(Arrays.asList("1", "2", "3"))); - ps.setObject(15, Value.stringArray(Arrays.asList("3", "2", "1"))); + index++, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99))); + ps.setObject(index++, Value.numeric(BigDecimal.TEN)); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + ps.setObject(index++, Value.json("{\"test_value\": \"foo\"}")); + } + ps.setObject(index++, Value.int64Array(new long[] {1L, 2L, 3L})); + ps.setObject(index++, Value.float64Array(new double[] {1.1D, 2.2D, 3.3D})); + ps.setObject(index++, Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE))); + ps.setObject(index++, Value.stringArray(Arrays.asList("1", "2", "3"))); + ps.setObject(index++, Value.stringArray(Arrays.asList("3", "2", "1"))); ps.setObject( - 16, + index++, Value.bytesArray( Arrays.asList( ByteArray.copyFrom("1"), ByteArray.copyFrom("2"), ByteArray.copyFrom("3")))); ps.setObject( - 17, + index++, Value.bytesArray( Arrays.asList( ByteArray.copyFrom("333"), ByteArray.copyFrom("222"), ByteArray.copyFrom("111")))); ps.setObject( - 18, + index++, Value.dateArray( Arrays.asList(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3), null))); ps.setObject( - 19, + index++, Value.timestampArray( Arrays.asList(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99), null))); - ps.setObject(20, Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN))); + ps.setObject( + index++, Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN))); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + ps.setObject( + index++, + Value.jsonArray(Arrays.asList("{\"key1\": \"val1\"}", null, "{\"key2\": \"val2\"}"))); + } assertEquals(1, ps.executeUpdate()); } try (ResultSet rs = con.createStatement() .executeQuery("SELECT * FROM TableWithAllColumnTypes WHERE ColInt64=2")) { assertTrue(rs.next()); - assertEquals(Value.int64(2L), rs.getObject(1, Value.class)); - assertEquals(Value.float64(2d), rs.getObject(2, Value.class)); - assertEquals(Value.bool(true), rs.getObject(3, Value.class)); - assertEquals(Value.string("testvalues"), rs.getObject(4, Value.class)); + int index = 1; + assertEquals(Value.int64(2L), rs.getObject(index++, Value.class)); + assertEquals(Value.float64(2d), rs.getObject(index++, Value.class)); + assertEquals(Value.bool(true), rs.getObject(index++, Value.class)); + assertEquals(Value.string("testvalues"), rs.getObject(index++, Value.class)); + assertEquals( + Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"), + rs.getObject(index++, Value.class)); + assertEquals(Value.bytes(ByteArray.copyFrom("test")), rs.getObject(index++, Value.class)); assertEquals( - Value.string("2d37f522-e0a5-4f22-8e09-4d77d299c967"), rs.getObject(5, Value.class)); - assertEquals(Value.bytes(ByteArray.copyFrom("test")), rs.getObject(6, Value.class)); - assertEquals(Value.bytes(ByteArray.copyFrom("testtest")), rs.getObject(7, Value.class)); + Value.bytes(ByteArray.copyFrom("testtest")), rs.getObject(index++, Value.class)); assertEquals( Value.date(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3)), - rs.getObject(8, Value.class)); + rs.getObject(index++, Value.class)); assertEquals( Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99)), - rs.getObject(9, Value.class)); - assertNotNull(rs.getObject(10, Value.class)); // Commit timestamp - assertEquals(Value.numeric(BigDecimal.TEN), rs.getObject(11, Value.class)); - assertEquals(Value.int64Array(new long[] {1L, 2L, 3L}), rs.getObject(12, Value.class)); + rs.getObject(index++, Value.class)); + assertNotNull(rs.getObject(index++, Value.class)); // Commit timestamp + assertEquals(Value.numeric(BigDecimal.TEN), rs.getObject(index++, Value.class)); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + assertEquals(Value.json("{\"test_value\":\"foo\"}"), rs.getObject(index++, Value.class)); + } + assertEquals(Value.int64Array(new long[] {1L, 2L, 3L}), rs.getObject(index++, Value.class)); assertEquals( - Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}), rs.getObject(13, Value.class)); + Value.float64Array(new double[] {1.1D, 2.2D, 3.3D}), + rs.getObject(index++, Value.class)); assertEquals( - Value.boolArray(Arrays.asList(true, null, false)), rs.getObject(14, Value.class)); + Value.boolArray(Arrays.asList(true, null, false)), rs.getObject(index++, Value.class)); assertEquals( - Value.stringArray(Arrays.asList("1", "2", "3")), rs.getObject(15, Value.class)); + Value.stringArray(Arrays.asList("1", "2", "3")), rs.getObject(index++, Value.class)); assertEquals( - Value.stringArray(Arrays.asList("3", "2", "1")), rs.getObject(16, Value.class)); + Value.stringArray(Arrays.asList("3", "2", "1")), rs.getObject(index++, Value.class)); assertEquals( Value.bytesArray( Arrays.asList( ByteArray.copyFrom("1"), ByteArray.copyFrom("2"), ByteArray.copyFrom("3"))), - rs.getObject(17, Value.class)); + rs.getObject(index++, Value.class)); assertEquals( Value.bytesArray( Arrays.asList( ByteArray.copyFrom("333"), ByteArray.copyFrom("222"), ByteArray.copyFrom("111"))), - rs.getObject(18, Value.class)); + rs.getObject(index++, Value.class)); assertEquals( Value.dateArray( Arrays.asList(com.google.cloud.Date.fromYearMonthDay(2021, 5, 3), null)), - rs.getObject(19, Value.class)); + rs.getObject(index++, Value.class)); assertEquals( Value.timestampArray( Arrays.asList(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(99999L, 99), null)), - rs.getObject(20, Value.class)); + rs.getObject(index++, Value.class)); assertEquals( Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)), - rs.getObject(21, Value.class)); + rs.getObject(index++, Value.class)); + if (!EmulatorSpannerHelper.isUsingEmulator()) { + assertEquals( + Value.jsonArray(Arrays.asList("{\"key1\":\"val1\"}", null, "{\"key2\":\"val2\"}")), + rs.getObject(index++, Value.class)); + } assertFalse(rs.next()); } } diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql index 6a8c4616..9b2cb3ba 100644 --- a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql +++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables.sql @@ -74,6 +74,7 @@ CREATE TABLE TableWithAllColumnTypes ( ColTimestamp TIMESTAMP NOT NULL, ColCommitTS TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), ColNumeric NUMERIC NOT NULL, + ColJson JSON NOT NULL, ColInt64Array ARRAY, ColFloat64Array ARRAY, @@ -85,6 +86,7 @@ CREATE TABLE TableWithAllColumnTypes ( ColDateArray ARRAY, ColTimestampArray ARRAY, ColNumericArray ARRAY, + ColJsonArray ARRAY, ColComputed STRING(MAX) AS (CONCAT(COALESCE(ColString, ''), ' ', COALESCE(ColStringMax, ''))) STORED, ) PRIMARY KEY (ColInt64) diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql new file mode 100644 index 00000000..d1e73bba --- /dev/null +++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_Emulator.sql @@ -0,0 +1,104 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +START BATCH DDL; + +CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX), + BirthDate DATE +) PRIMARY KEY(SingerId); + +CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName); + +CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX), + MarketingBudget INT64 +) PRIMARY KEY(SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE; + +CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle); + +CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget); + +CREATE TABLE Songs ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + TrackId INT64 NOT NULL, + SongName STRING(MAX), + Duration INT64, + SongGenre STRING(25) +) PRIMARY KEY(SingerId, AlbumId, TrackId), + INTERLEAVE IN PARENT Albums ON DELETE CASCADE; + +CREATE UNIQUE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC), INTERLEAVE IN Albums; + +CREATE INDEX SongsBySongName ON Songs(SongName); + +CREATE TABLE Concerts ( + VenueId INT64 NOT NULL, + SingerId INT64 NOT NULL, + ConcertDate DATE NOT NULL, + BeginTime TIMESTAMP, + EndTime TIMESTAMP, + TicketPrices ARRAY, + CONSTRAINT Fk_Concerts_Singer FOREIGN KEY (SingerId) REFERENCES Singers (SingerId) +) PRIMARY KEY(VenueId, SingerId, ConcertDate); + +CREATE TABLE TableWithAllColumnTypes ( + ColInt64 INT64 NOT NULL, + ColFloat64 FLOAT64 NOT NULL, + ColBool BOOL NOT NULL, + ColString STRING(100) NOT NULL, + ColStringMax STRING(MAX) NOT NULL, + ColBytes BYTES(100) NOT NULL, + ColBytesMax BYTES(MAX) NOT NULL, + ColDate DATE NOT NULL, + ColTimestamp TIMESTAMP NOT NULL, + ColCommitTS TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true), + ColNumeric NUMERIC NOT NULL, + + ColInt64Array ARRAY, + ColFloat64Array ARRAY, + ColBoolArray ARRAY, + ColStringArray ARRAY, + ColStringMaxArray ARRAY, + ColBytesArray ARRAY, + ColBytesMaxArray ARRAY, + ColDateArray ARRAY, + ColTimestampArray ARRAY, + ColNumericArray ARRAY, + + ColComputed STRING(MAX) AS (CONCAT(COALESCE(ColString, ''), ' ', COALESCE(ColStringMax, ''))) STORED, +) PRIMARY KEY (ColInt64) +; + +CREATE TABLE TableWithRef ( + Id INT64 NOT NULL, + RefFloat FLOAT64 NOT NULL, + RefString STRING(100) NOT NULL, + RefDate DATE NOT NULL, + CONSTRAINT Fk_TableWithRef_TableWithAllColumnTypes + FOREIGN KEY (RefFloat, RefString, RefDate) + REFERENCES TableWithAllColumnTypes (ColFloat64, ColString, ColDate) +) PRIMARY KEY (Id) +; + +RUN BATCH; \ No newline at end of file