From cefc290d343a2a973e1efbeee33c349fbf98060c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 7 Nov 2022 14:44:14 +0100 Subject: [PATCH] feat: jsonb data type support (#926) Add support for the jsonb data type for PostgreSQL dialect databases. --- .../spanner/jdbc/AbstractJdbcWrapper.java | 105 ++++-- .../google/cloud/spanner/jdbc/JdbcArray.java | 3 + .../cloud/spanner/jdbc/JdbcDataType.java | 31 ++ .../spanner/jdbc/JdbcDatabaseMetaData.java | 48 ++- .../spanner/jdbc/JdbcParameterStore.java | 15 + .../cloud/spanner/jdbc/JdbcResultSet.java | 16 + .../spanner/jdbc/JdbcResultSetMetaData.java | 4 +- .../cloud/spanner/jdbc/JdbcTypeConverter.java | 10 +- .../cloud/spanner/jdbc/PgJsonbType.java | 51 +++ .../DatabaseMetaData_GetColumns.sql | 2 +- .../cloud/spanner/jdbc/JdbcArrayTest.java | 19 + .../jdbc/JdbcDatabaseMetaDataTest.java | 51 +-- .../spanner/jdbc/JdbcParameterStoreTest.java | 37 +- .../jdbc/JdbcPreparedStatementTest.java | 10 +- .../jdbc/JdbcResultSetMetaDataTest.java | 356 +++++++++++++----- .../jdbc/it/ITJdbcPgDatabaseMetaDataTest.java | 6 +- .../jdbc/it/ITJdbcPreparedStatementTest.java | 46 +++ .../spanner/jdbc/it/CreateMusicTables_PG.sql | 4 +- 18 files changed, 644 insertions(+), 170 deletions(-) create mode 100644 src/main/java/com/google/cloud/spanner/jdbc/PgJsonbType.java 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 ce1f84760c2..f577ac3c658 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java @@ -41,18 +41,32 @@ abstract class AbstractJdbcWrapper implements Wrapper { */ static int extractColumnType(Type type) { Preconditions.checkNotNull(type); - if (type.equals(Type.bool())) return Types.BOOLEAN; - if (type.equals(Type.bytes())) return Types.BINARY; - if (type.equals(Type.date())) return Types.DATE; - if (type.equals(Type.float64())) return Types.DOUBLE; - if (type.equals(Type.int64())) return Types.BIGINT; - if (type.equals(Type.numeric())) return Types.NUMERIC; - if (type.equals(Type.pgNumeric())) 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; + switch (type.getCode()) { + case BOOL: + return Types.BOOLEAN; + case BYTES: + return Types.BINARY; + case DATE: + return Types.DATE; + case FLOAT64: + return Types.DOUBLE; + case INT64: + return Types.BIGINT; + case NUMERIC: + case PG_NUMERIC: + return Types.NUMERIC; + case STRING: + case JSON: + case PG_JSONB: + return Types.NVARCHAR; + case TIMESTAMP: + return Types.TIMESTAMP; + case ARRAY: + return Types.ARRAY; + case STRUCT: + default: + return Types.OTHER; + } } /** Extract Spanner type name from {@link java.sql.Types} code. */ @@ -101,29 +115,52 @@ static String getClassName(int sqlType) { */ static String getClassName(Type type) { Preconditions.checkNotNull(type); - if (type == Type.bool()) return Boolean.class.getName(); - if (type == Type.bytes()) return byte[].class.getName(); - if (type == Type.date()) return Date.class.getName(); - if (type == Type.float64()) return Double.class.getName(); - if (type == Type.int64()) return Long.class.getName(); - if (type == Type.numeric()) return BigDecimal.class.getName(); - if (type == Type.pgNumeric()) 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(); - if (type.getArrayElementType() == Type.bytes()) return byte[][].class.getName(); - if (type.getArrayElementType() == Type.date()) return Date[].class.getName(); - if (type.getArrayElementType() == Type.float64()) return Double[].class.getName(); - if (type.getArrayElementType() == Type.int64()) return Long[].class.getName(); - if (type.getArrayElementType() == Type.numeric()) return BigDecimal[].class.getName(); - if (type.getArrayElementType() == Type.pgNumeric()) 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(); + switch (type.getCode()) { + case BOOL: + return Boolean.class.getName(); + case BYTES: + return byte[].class.getName(); + case DATE: + return Date.class.getName(); + case FLOAT64: + return Double.class.getName(); + case INT64: + return Long.class.getName(); + case NUMERIC: + case PG_NUMERIC: + return BigDecimal.class.getName(); + case STRING: + case JSON: + case PG_JSONB: + return String.class.getName(); + case TIMESTAMP: + return Timestamp.class.getName(); + case ARRAY: + switch (type.getArrayElementType().getCode()) { + case BOOL: + return Boolean[].class.getName(); + case BYTES: + return byte[][].class.getName(); + case DATE: + return Date[].class.getName(); + case FLOAT64: + return Double[].class.getName(); + case INT64: + return Long[].class.getName(); + case NUMERIC: + case PG_NUMERIC: + return BigDecimal[].class.getName(); + case STRING: + case JSON: + case PG_JSONB: + return String[].class.getName(); + case TIMESTAMP: + return Timestamp[].class.getName(); + } + case STRUCT: + default: + return null; } - return null; } /** Standard error message for out-of-range values. */ 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 f712b56ca72..650c1eb78f0 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java @@ -203,6 +203,9 @@ public ResultSet getResultSet(long startIndex, int count) throws SQLException { case JSON: builder = binder.to(Value.json((String) value)); break; + case PG_JSONB: + builder = binder.to(Value.pgJsonb((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 7eecb2a678d..c495bbe1660 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java @@ -281,6 +281,37 @@ public Type getSpannerType() { return Type.json(); } }, + PG_JSONB { + @Override + public int getSqlType() { + return PgJsonbType.VENDOR_TYPE_NUMBER; + } + + @Override + public Class getJavaClass() { + return String.class; + } + + @Override + public Code getCode() { + return Code.PG_JSONB; + } + + @Override + public List getArrayElements(ResultSet rs, int columnIndex) { + return rs.getPgJsonbList(columnIndex); + } + + @Override + public String getTypeName() { + return "JSONB"; + } + + @Override + public Type getSpannerType() { + return Type.pgJsonb(); + } + }, TIMESTAMP { @Override public int getSqlType() { diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java index 66c76b53207..b9c7cb8639d 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaData.java @@ -940,6 +940,7 @@ public ResultSet getTypeInfo() { StructField.of("SQL_DATETIME_SUB", Type.int64()), StructField.of("NUM_PREC_RADIX", Type.int64())), Arrays.asList( + // TODO(#925): Make these dialect-dependent (i.e. 'timestamptz' for PostgreSQL. Struct.newBuilder() .set("TYPE_NAME") .to("STRING") @@ -1243,7 +1244,52 @@ public ResultSet getTypeInfo() { .to((Long) null) .set("NUM_PREC_RADIX") .to(10) - .build()))); + .build(), + getJsonType(connection.getDialect())))); + } + + private Struct getJsonType(Dialect dialect) { + return Struct.newBuilder() + .set("TYPE_NAME") + .to(dialect == Dialect.POSTGRESQL ? "JSONB" : "JSON") + .set("DATA_TYPE") + .to( + dialect == Dialect.POSTGRESQL + ? PgJsonbType.VENDOR_TYPE_NUMBER + : JsonType.VENDOR_TYPE_NUMBER) + .set("PRECISION") + .to(2621440L) + .set("LITERAL_PREFIX") + .to((String) null) + .set("LITERAL_SUFFIX") + .to((String) null) + .set("CREATE_PARAMS") + .to((String) null) + .set("NULLABLE") + .to(DatabaseMetaData.typeNullable) + .set("CASE_SENSITIVE") + .to(true) + .set("SEARCHABLE") + .to(DatabaseMetaData.typeSearchable) + .set("UNSIGNED_ATTRIBUTE") + .to(true) + .set("FIXED_PREC_SCALE") + .to(false) + .set("AUTO_INCREMENT") + .to(false) + .set("LOCAL_TYPE_NAME") + .to(dialect == Dialect.POSTGRESQL ? "JSONB" : "JSON") + .set("MINIMUM_SCALE") + .to(0) + .set("MAXIMUM_SCALE") + .to(0) + .set("SQL_DATA_TYPE") + .to((Long) null) + .set("SQL_DATETIME_SUB") + .to((Long) null) + .set("NUM_PREC_RADIX") + .to((Long) null) + .build(); } @Override 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 8be9e6fc331..3ce105aa747 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java @@ -273,6 +273,7 @@ private boolean isTypeSupported(int sqlType) { case Types.NUMERIC: case Types.DECIMAL: case JsonType.VENDOR_TYPE_NUMBER: + case PgJsonbType.VENDOR_TYPE_NUMBER: return true; } return false; @@ -336,6 +337,12 @@ private boolean isValidTypeAndValue(Object value, int sqlType) { || value instanceof InputStream || value instanceof Reader || (value instanceof Value && ((Value) value).getType().getCode() == Type.Code.JSON); + case PgJsonbType.VENDOR_TYPE_NUMBER: + return value instanceof String + || value instanceof InputStream + || value instanceof Reader + || (value instanceof Value + && ((Value) value).getType().getCode() == Type.Code.PG_JSONB); } return false; } @@ -490,6 +497,7 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value, } return binder.to(stringValue); case JsonType.VENDOR_TYPE_NUMBER: + case PgJsonbType.VENDOR_TYPE_NUMBER: String jsonValue; if (value instanceof String) { jsonValue = (String) value; @@ -501,6 +509,9 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value, throw JdbcSqlExceptionFactory.of( value + " is not a valid JSON value", Code.INVALID_ARGUMENT); } + if (sqlType == PgJsonbType.VENDOR_TYPE_NUMBER) { + return binder.to(Value.pgJsonb(jsonValue)); + } return binder.to(Value.json(jsonValue)); case Types.DATE: if (value instanceof Date) { @@ -750,6 +761,8 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu return binder.toStringArray(null); case JsonType.VENDOR_TYPE_NUMBER: return binder.toJsonArray(null); + case PgJsonbType.VENDOR_TYPE_NUMBER: + return binder.toPgJsonbArray(null); case Types.DATE: return binder.toDateArray(null); case Types.TIME: @@ -818,6 +831,8 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu } else if (String[].class.isAssignableFrom(value.getClass())) { if (type == JsonType.VENDOR_TYPE_NUMBER) { return binder.toJsonArray(Arrays.asList((String[]) value)); + } else if (type == PgJsonbType.VENDOR_TYPE_NUMBER) { + return binder.toPgJsonbArray(Arrays.asList((String[]) value)); } else { return binder.toStringArray(Arrays.asList((String[]) 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 089ef7b2161..f8ab452640d 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java @@ -155,6 +155,8 @@ public String getString(int columnIndex) throws SQLException { return isNull ? null : spanner.getString(spannerIndex); case JSON: return isNull ? null : spanner.getJson(spannerIndex); + case PG_JSONB: + return isNull ? null : spanner.getPgJsonb(spannerIndex); case TIMESTAMP: return isNull ? null : spanner.getTimestamp(spannerIndex).toString(); case STRUCT: @@ -185,6 +187,7 @@ public boolean getBoolean(int columnIndex) throws SQLException { return !isNull && Boolean.parseBoolean(spanner.getString(spannerIndex)); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -219,6 +222,7 @@ public byte getByte(int columnIndex) throws SQLException { return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex))); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -253,6 +257,7 @@ public short getShort(int columnIndex) throws SQLException { return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex))); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -287,6 +292,7 @@ public int getInt(int columnIndex) throws SQLException { return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex))); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -319,6 +325,7 @@ public long getLong(int columnIndex) throws SQLException { return isNull ? 0L : parseLong(spanner.getString(spannerIndex)); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -349,6 +356,7 @@ public float getFloat(int columnIndex) throws SQLException { return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex))); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -379,6 +387,7 @@ public double getDouble(int columnIndex) throws SQLException { return isNull ? 0 : parseDouble(spanner.getString(spannerIndex)); case BYTES: case JSON: + case PG_JSONB: case DATE: case STRUCT: case TIMESTAMP: @@ -418,6 +427,7 @@ public Date getDate(int columnIndex) throws SQLException { case PG_NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: @@ -444,6 +454,7 @@ public Time getTime(int columnIndex) throws SQLException { case PG_NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: @@ -471,6 +482,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException { case PG_NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: @@ -726,6 +738,7 @@ private BigDecimal getBigDecimal(int columnIndex, boolean fixedScale, int scale) } case BYTES: case JSON: + case PG_JSONB: case DATE: case TIMESTAMP: case STRUCT: @@ -818,6 +831,7 @@ public Date getDate(int columnIndex, Calendar cal) throws SQLException { case NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: @@ -848,6 +862,7 @@ public Time getTime(int columnIndex, Calendar cal) throws SQLException { case NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: @@ -881,6 +896,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException case NUMERIC: case BYTES: case JSON: + case PG_JSONB: case STRUCT: case ARRAY: default: diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java index ae312e90971..26226bc6631 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java @@ -155,7 +155,9 @@ public int getPrecision(int column) { @Override public int getScale(int column) { int colType = getColumnType(column); - if (colType == Types.DOUBLE || colType == Types.NUMERIC) return 15; + if (colType == Types.DOUBLE || colType == Types.NUMERIC) { + return 15; + } return 0; } 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 f6cba1dcd20..a506998ea6c 100644 --- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java +++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java @@ -84,8 +84,9 @@ 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 || type.getCode() == Code.JSON) - return ((String) value).getBytes(UTF8); + if (type.getCode() == Code.STRING + || type.getCode() == Code.JSON + || type.getCode() == Code.PG_JSONB) return ((String) value).getBytes(UTF8); } if (targetType.equals(Boolean.class)) { if (type.getCode() == Code.BOOL) return value; @@ -207,6 +208,9 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx toGoogleTimestamps((java.sql.Timestamp[]) ((java.sql.Array) value).getArray())); case JSON: return Value.jsonArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray())); + case PG_JSONB: + return Value.pgJsonbArray( + Arrays.asList((String[]) ((java.sql.Array) value).getArray())); case STRUCT: default: throw JdbcSqlExceptionFactory.of( @@ -232,6 +236,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx return Value.timestamp(toGoogleTimestamp((java.sql.Timestamp) value)); case JSON: return Value.json((String) value); + case PG_JSONB: + return Value.pgJsonb((String) value); case STRUCT: default: throw JdbcSqlExceptionFactory.of( diff --git a/src/main/java/com/google/cloud/spanner/jdbc/PgJsonbType.java b/src/main/java/com/google/cloud/spanner/jdbc/PgJsonbType.java new file mode 100644 index 00000000000..8627d341391 --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/jdbc/PgJsonbType.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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.SQLType; + +public class PgJsonbType implements SQLType { + public static final PgJsonbType INSTANCE = new PgJsonbType(); + /** + * Spanner/Spangres does not have any type numbers, but the code values are unique. Add 200,000 to + * avoid conflicts with the type numbers in java.sql.Types. Native Cloud Spanner types already use + * the range starting at 100,000 (see {@link JsonType}). + */ + public static final int VENDOR_TYPE_NUMBER = 200_000 + TypeCode.JSON_VALUE; + + private PgJsonbType() {} + + @Override + public String getName() { + return "JSONB"; + } + + @Override + public String getVendor() { + return PgJsonbType.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/postgresql/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql index 2c3c2e4762b..5b37e32ecad 100644 --- a/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql +++ b/src/main/resources/com/google/cloud/spanner/jdbc/postgresql/DatabaseMetaData_GetColumns.sql @@ -37,7 +37,7 @@ SELECT TABLE_CATALOG AS "TABLE_CAT", TABLE_SCHEMA AS "TABLE_SCHEM", TABLE_NAME A WHEN DATA_TYPE = 'bigint' THEN 19 WHEN DATA_TYPE = 'numeric' THEN 15 WHEN DATA_TYPE LIKE 'character varying' THEN CHARACTER_MAXIMUM_LENGTH - WHEN DATA_TYPE = 'jsonb' THEN 2621440 + WHEN DATA_TYPE = 'jsonb' THEN CHARACTER_MAXIMUM_LENGTH WHEN DATA_TYPE = 'timestamp with time zone' THEN 35 END AS "COLUMN_SIZE", 0 AS "BUFFER_LENGTH", diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java index 5888b3b4008..16e16ad06c3 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcArrayTest.java @@ -169,6 +169,25 @@ public void testCreateArrayTypeName() throws SQLException { assertFalse(rs.next()); } + array = + JdbcArray.createArray( + "JSONB", + new String[] {"{}", "[]", null, "{\"name\":\"John\", \"age\":30, \"car\":null}"}); + assertThat(array.getBaseType()).isEqualTo(PgJsonbType.VENDOR_TYPE_NUMBER); + assertThat(((String[]) array.getArray(1, 1))[0]).isEqualTo("{}"); + try (ResultSet rs = array.getResultSet()) { + assertTrue(rs.next()); + assertEquals("{}", rs.getString(2)); + assertTrue(rs.next()); + assertEquals("[]", rs.getString(2)); + assertTrue(rs.next()); + assertNull(rs.getString(2)); + assertTrue(rs.wasNull()); + assertTrue(rs.next()); + assertEquals("{\"name\":\"John\", \"age\":30, \"car\":null}", rs.getString(2)); + assertFalse(rs.next()); + } + array = JdbcArray.createArray( "TIMESTAMP", diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java index 0d977c9959b..6a2b0cac060 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataTest.java @@ -22,10 +22,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.cloud.spanner.connection.ConnectionOptionsTest; import java.io.IOException; @@ -462,28 +464,33 @@ public void testGetTableTypes() throws SQLException { @Test public void testGetTypeInfo() throws SQLException { - JdbcConnection connection = mock(JdbcConnection.class); - DatabaseMetaData meta = new JdbcDatabaseMetaData(connection); - try (ResultSet rs = meta.getTypeInfo()) { - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("STRING"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("INT64"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("BYTES"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("FLOAT64"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("BOOL"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("DATE"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("TIMESTAMP"))); - assertThat(rs.next(), is(true)); - assertThat(rs.getString("TYPE_NAME"), is(equalTo("NUMERIC"))); - assertThat(rs.next(), is(false)); - ResultSetMetaData rsmd = rs.getMetaData(); - assertThat(rsmd.getColumnCount(), is(equalTo(18))); + for (Dialect dialect : Dialect.values()) { + JdbcConnection connection = mock(JdbcConnection.class); + when(connection.getDialect()).thenReturn(dialect); + DatabaseMetaData meta = new JdbcDatabaseMetaData(connection); + try (ResultSet rs = meta.getTypeInfo()) { + assertTrue(rs.next()); + assertEquals("STRING", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("INT64", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("BYTES", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("FLOAT64", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("BOOL", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("DATE", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("TIMESTAMP", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals("NUMERIC", rs.getString("TYPE_NAME")); + assertTrue(rs.next()); + assertEquals(dialect == Dialect.POSTGRESQL ? "JSONB" : "JSON", rs.getString("TYPE_NAME")); + assertThat(rs.next(), is(false)); + ResultSetMetaData rsmd = rs.getMetaData(); + assertThat(rsmd.getColumnCount(), is(equalTo(18))); + } } } diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java index 7d966b4f6a7..869655bb39c 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java @@ -226,6 +226,14 @@ public void testSetParameterWithType() throws SQLException, IOException { assertEquals(jsonString, params.getParameter(1)); verifyParameter(params, Value.json(jsonString)); + params.setParameter(1, jsonString, PgJsonbType.VENDOR_TYPE_NUMBER); + assertEquals(jsonString, params.getParameter(1)); + verifyParameter(params, Value.pgJsonb(jsonString)); + + params.setParameter(1, jsonString, PgJsonbType.INSTANCE); + assertEquals(jsonString, params.getParameter(1)); + verifyParameter(params, Value.pgJsonb(jsonString)); + params.setParameter(1, BigDecimal.ONE, Types.DECIMAL); if (dialect == Dialect.POSTGRESQL) { verifyParameter(params, Value.pgNumeric(BigDecimal.ONE.toString())); @@ -476,7 +484,8 @@ public void testSetInvalidParameterWithType() throws SQLException, IOException { Types.NCHAR, Types.NVARCHAR, Types.LONGNVARCHAR, - JsonType.VENDOR_TYPE_NUMBER + JsonType.VENDOR_TYPE_NUMBER, + PgJsonbType.VENDOR_TYPE_NUMBER }) { assertInvalidParameter(params, new Object(), type); assertInvalidParameter(params, Boolean.TRUE, type); @@ -502,7 +511,8 @@ public void testSetInvalidParameterWithType() throws SQLException, IOException { Types.NCHAR, Types.NVARCHAR, Types.LONGNVARCHAR, - JsonType.VENDOR_TYPE_NUMBER + JsonType.VENDOR_TYPE_NUMBER, + PgJsonbType.VENDOR_TYPE_NUMBER }) { Reader reader = new StringReader("test"); reader.close(); @@ -595,6 +605,10 @@ public void testSetParameterWithoutType() throws SQLException { params.setParameter(1, Value.json(jsonString), (Integer) null); assertEquals(Value.json(jsonString), params.getParameter(1)); verifyParameter(params, Value.json(jsonString)); + + params.setParameter(1, Value.pgJsonb(jsonString), (Integer) null); + assertEquals(Value.pgJsonb(jsonString), params.getParameter(1)); + verifyParameter(params, Value.pgJsonb(jsonString)); } private boolean stringReadersEqual(StringReader r1, StringReader r2) throws IOException { @@ -772,6 +786,25 @@ public void testSetArrayParameter() throws SQLException { assertEquals( Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null)), params.getParameter(1)); verifyParameter(params, Value.jsonArray(Arrays.asList(jsonString1, jsonString2, null))); + + // JSONB + params.setParameter( + 1, + JdbcArray.createArray("JSONB", new String[] {jsonString1, jsonString2, null}), + Types.ARRAY); + assertEquals( + JdbcArray.createArray("JSONB", new String[] {jsonString1, jsonString2, null}), + params.getParameter(1)); + verifyParameter(params, Value.pgJsonbArray(Arrays.asList(jsonString1, jsonString2, null))); + + params.setParameter(1, JdbcArray.createArray("JSONB", null), Types.ARRAY); + assertEquals(JdbcArray.createArray("JSONB", null), params.getParameter(1)); + verifyParameter(params, Value.pgJsonbArray(null)); + + params.setParameter(1, Value.pgJsonbArray(Arrays.asList(jsonString1, jsonString2, null))); + assertEquals( + Value.pgJsonbArray(Arrays.asList(jsonString1, jsonString2, null)), params.getParameter(1)); + verifyParameter(params, Value.pgJsonbArray(Arrays.asList(jsonString1, jsonString2, null))); } private void verifyParameter(JdbcParameterStore params, Value value) throws SQLException { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java index 99f8974159c..b4c33a802f9 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java @@ -128,6 +128,8 @@ public void testValueAsParameter() throws SQLException { Value.int64(13L), Value.numeric(new BigDecimal("3.14")), Value.string("bar"), + Value.json("{}"), + Value.pgJsonb("{}"), Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(999L, 99)), Value.boolArray(Collections.singleton(true)), Value.bytesArray(Collections.singleton(ByteArray.copyFrom("foo"))), @@ -137,6 +139,8 @@ public void testValueAsParameter() throws SQLException { Value.int64Array(Collections.singleton(13L)), Value.numericArray(Collections.singleton(new BigDecimal("3.14"))), Value.stringArray(Collections.singleton("bar")), + Value.jsonArray(Collections.singleton("{}")), + Value.pgJsonbArray(Collections.singleton("{}")), Value.timestampArray( Collections.singleton(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(999L, 99))), }) { @@ -152,7 +156,7 @@ public void testValueAsParameter() throws SQLException { @SuppressWarnings("deprecation") @Test public void testParameters() throws SQLException, MalformedURLException { - final int numberOfParams = 51; + final int numberOfParams = 53; String sql = generateSqlWithParameters(numberOfParams); JdbcConnection connection = createMockConnection(); @@ -204,6 +208,8 @@ public void testParameters() throws SQLException, MalformedURLException { ps.setObject(49, UUID.fromString("83b988cf-1f4e-428a-be3d-cc712621942e")); ps.setObject(50, "TEST", JDBCType.NVARCHAR); ps.setObject(51, "TEST", JDBCType.NVARCHAR, 20); + ps.setObject(52, "{}", JsonType.VENDOR_TYPE_NUMBER); + ps.setObject(53, "{}", PgJsonbType.VENDOR_TYPE_NUMBER); testSetUnsupportedTypes(ps); @@ -259,6 +265,8 @@ public void testParameters() throws SQLException, MalformedURLException { assertEquals(UUID.class.getName(), pmd.getParameterClassName(49)); assertEquals(String.class.getName(), pmd.getParameterClassName(50)); assertEquals(String.class.getName(), pmd.getParameterClassName(51)); + assertEquals(String.class.getName(), pmd.getParameterClassName(51)); + assertEquals(String.class.getName(), pmd.getParameterClassName(51)); ps.clearParameters(); pmd = ps.getParameterMetaData(); diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java index 559ad293588..a46a1c32403 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaDataTest.java @@ -27,10 +27,10 @@ import com.google.cloud.spanner.ResultSets; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; -import com.google.cloud.spanner.Type.Code; import com.google.cloud.spanner.Type.StructField; import com.google.cloud.spanner.Value; import com.google.common.base.Preconditions; +import java.math.BigDecimal; import java.sql.Date; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -67,15 +67,31 @@ private TestColumn(Type type, String name, Integer nulls, boolean calculated) { } private static int getDefaultSize(Type type) { - if (type == Type.bool()) return 1; - if (type == Type.date()) return 10; - if (type == Type.float64()) return 14; - if (type == Type.int64()) return 10; - if (type == Type.timestamp()) return 24; - if (type == Type.string()) return 50; - if (type == Type.json()) return 50; - if (type == Type.bytes()) return 50; - return 50; + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case BOOL: + return 1; + case DATE: + return 10; + case FLOAT64: + return 14; + case INT64: + return 10; + case TIMESTAMP: + return 24; + case STRING: + case BYTES: + case JSON: + case PG_JSONB: + return 50; + case NUMERIC: + case PG_NUMERIC: + return 50; + case ARRAY: + case STRUCT: + default: + return 50; + } } private boolean isTableColumn() { @@ -177,7 +193,10 @@ private static List getAllTypes() { types.add(Type.int64()); types.add(Type.string()); types.add(Type.json()); + types.add(Type.pgJsonb()); types.add(Type.timestamp()); + types.add(Type.numeric()); + types.add(Type.pgNumeric()); List arrayTypes = new ArrayList<>(); for (Type type : types) { arrayTypes.add(Type.array(type)); @@ -208,42 +227,70 @@ private JdbcResultSet getFooTestResultSet(Statement statement) { } private Value getDefaultValue(Type type, int row) { - if (type == Type.bool()) return Value.bool(Boolean.TRUE); - if (type == Type.bytes()) return Value.bytes(ByteArray.copyFrom("test byte array " + row)); - if (type == Type.date()) return Value.date(com.google.cloud.Date.fromYearMonthDay(2018, 4, 1)); - 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) { - if (type.getArrayElementType() == Type.bool()) - return Value.boolArray(Arrays.asList(Boolean.TRUE, Boolean.FALSE)); - if (type.getArrayElementType() == Type.bytes()) - return Value.bytesArray( - Arrays.asList( - ByteArray.copyFrom("test byte array " + row), - ByteArray.copyFrom("test byte array " + row))); - if (type.getArrayElementType() == Type.date()) - return Value.dateArray( - Arrays.asList( - com.google.cloud.Date.fromYearMonthDay(2018, 4, 1), - com.google.cloud.Date.fromYearMonthDay(2018, 4, 2))); - if (type.getArrayElementType() == Type.float64()) - return Value.float64Array(Arrays.asList(123.45D, 543.21D)); - if (type.getArrayElementType() == Type.int64()) - 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())); - } - return null; + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case BOOL: + return Value.bool(Boolean.TRUE); + case INT64: + return Value.int64(12345L); + case NUMERIC: + return Value.numeric(new BigDecimal("3.14")); + case PG_NUMERIC: + return Value.pgNumeric("3.14"); + case FLOAT64: + return Value.float64(123.45D); + case STRING: + return Value.string("test value " + row); + case JSON: + return Value.json("{\"test_value\": " + row + "}"); + case PG_JSONB: + return Value.pgJsonb("{\"test_value\": " + row + "}"); + case BYTES: + return Value.bytes(ByteArray.copyFrom("test byte array " + row)); + case TIMESTAMP: + return Value.timestamp(com.google.cloud.Timestamp.now()); + case DATE: + return Value.date(com.google.cloud.Date.fromYearMonthDay(2018, 4, 1)); + case ARRAY: + switch (type.getArrayElementType().getCode()) { + case BOOL: + return Value.boolArray(Arrays.asList(Boolean.TRUE, Boolean.FALSE)); + case INT64: + return Value.int64Array(Arrays.asList(12345L, 54321L)); + case NUMERIC: + return Value.numericArray(Arrays.asList(BigDecimal.ONE, BigDecimal.TEN)); + case PG_NUMERIC: + return Value.pgNumericArray(Arrays.asList("3.14", null, "NaN", "6.626")); + case FLOAT64: + return Value.float64Array(Arrays.asList(123.45D, 543.21D)); + case STRING: + return Value.stringArray(Arrays.asList("test value " + row, "test value " + row)); + case JSON: + return Value.jsonArray( + Arrays.asList("{\"test_value\": " + row + "}", "{\"test_value\": " + row + "}")); + case PG_JSONB: + return Value.pgJsonbArray( + Arrays.asList("{\"test_value\": " + row + "}", "{\"test_value\": " + row + "}")); + case BYTES: + return Value.bytesArray( + Arrays.asList( + ByteArray.copyFrom("test byte array " + row), + ByteArray.copyFrom("test byte array " + row))); + case TIMESTAMP: + return Value.timestampArray( + Arrays.asList(com.google.cloud.Timestamp.now(), com.google.cloud.Timestamp.now())); + case DATE: + return Value.dateArray( + Arrays.asList( + com.google.cloud.Date.fromYearMonthDay(2018, 4, 1), + com.google.cloud.Date.fromYearMonthDay(2018, 4, 2))); + case ARRAY: + case STRUCT: + } + case STRUCT: + default: + return null; + } } @Test @@ -263,7 +310,10 @@ public void testIsCaseSensitive() { for (int i = 1; i <= TEST_COLUMNS.size(); i++) { Type type = TEST_COLUMNS.get(i - 1).type; assertEquals( - type == Type.string() || type == Type.bytes() || type == Type.json(), + type == Type.string() + || type == Type.bytes() + || type == Type.json() + || type == Type.pgJsonb(), subject.isCaseSensitive(i)); } } @@ -293,7 +343,10 @@ public void testIsNullable() { public void testIsSigned() { for (int i = 1; i <= TEST_COLUMNS.size(); i++) { Type type = TEST_COLUMNS.get(i - 1).type; - if (type == Type.int64() || type == Type.float64()) { + if (type == Type.int64() + || type == Type.float64() + || type == Type.numeric() + || type == Type.pgNumeric()) { assertTrue(subject.isSigned(i)); } else { assertFalse(subject.isSigned(i)); @@ -305,24 +358,42 @@ public void testIsSigned() { public void testGetColumnDisplaySize() { for (int i = 1; i <= TEST_COLUMNS.size(); i++) { assertEquals( - getDefaultDisplaySize(TEST_COLUMNS.get(i - 1).type, i), subject.getColumnDisplaySize(i)); + "Wrong column display size for " + TEST_COLUMNS.get(i - 1).type, + getDefaultDisplaySize(TEST_COLUMNS.get(i - 1).type, i), + subject.getColumnDisplaySize(i)); } } private int getDefaultDisplaySize(Type type, int column) { - if (type.getCode() == Code.ARRAY) return 50; - if (type == Type.bool()) return 5; - if (type == Type.bytes()) return 50; - if (type == Type.date()) return 10; - if (type == Type.float64()) return 14; - if (type == Type.int64()) return 10; - if (type == Type.string()) { - int length = subject.getPrecision(column); - return length == 0 ? 50 : length; + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case BOOL: + return 5; + case INT64: + return 10; + case NUMERIC: + case PG_NUMERIC: + return 14; + case FLOAT64: + return 14; + case STRING: + int length = subject.getPrecision(column); + return length == 0 ? 50 : length; + case JSON: + case PG_JSONB: + return 50; + case BYTES: + return 50; + case TIMESTAMP: + return 16; + case DATE: + return 10; + case ARRAY: + return 50; + case STRUCT: + default: + return 10; } - if (type == Type.json()) return 50; - if (type == Type.timestamp()) return 16; - return 10; } @Test @@ -347,18 +418,38 @@ public void testGetSchemaName() throws SQLException { @Test public void testGetPrecision() { for (int i = 1; i <= TEST_COLUMNS.size(); i++) { - assertEquals(getPrecision(TEST_COLUMNS.get(i - 1)), subject.getPrecision(i)); + assertEquals( + "Wrong precision for type " + TEST_COLUMNS.get(i - 1).type, + getPrecision(TEST_COLUMNS.get(i - 1)), + subject.getPrecision(i)); } } private int getPrecision(TestColumn col) { - if (col.type == Type.bool()) return 1; - if (col.type == Type.date()) return 10; - if (col.type == Type.float64()) return 14; - if (col.type == Type.int64()) return 10; - if (col.type == Type.timestamp()) return 24; - if (col.isTableColumn()) return col.defaultSize; - return 50; + Preconditions.checkNotNull(col); + switch (col.type.getCode()) { + case BOOL: + return 1; + case DATE: + return 10; + case FLOAT64: + return 14; + case INT64: + return 10; + case TIMESTAMP: + return 24; + case NUMERIC: + case PG_NUMERIC: + return 14; + case STRING: + case JSON: + case PG_JSONB: + case BYTES: + case ARRAY: + case STRUCT: + default: + return col.isTableColumn() ? col.defaultSize : 50; + } } @Test @@ -369,7 +460,9 @@ public void testGetScale() { } private int getScale(TestColumn col) { - if (col.type == Type.float64()) return 15; + if (col.type == Type.float64() || col.type == Type.numeric() || col.type == Type.pgNumeric()) { + return 15; + } return 0; } @@ -393,16 +486,33 @@ public void testGetColumnType() { } private int getSqlType(Type type) { - if (type == Type.bool()) return Types.BOOLEAN; - if (type == Type.bytes()) return Types.BINARY; - if (type == Type.date()) return Types.DATE; - 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; + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case BOOL: + return Types.BOOLEAN; + case INT64: + return Types.BIGINT; + case NUMERIC: + case PG_NUMERIC: + return Types.NUMERIC; + case FLOAT64: + return Types.DOUBLE; + case STRING: + case JSON: + case PG_JSONB: + return Types.NVARCHAR; + case BYTES: + return Types.BINARY; + case TIMESTAMP: + return Types.TIMESTAMP; + case DATE: + return Types.DATE; + case ARRAY: + return Types.ARRAY; + case STRUCT: + default: + return Types.OTHER; + } } @Test @@ -443,25 +553,57 @@ public void testGetColumnClassName() { } private String getTypeClassName(Type type) { - if (type == Type.bool()) return Boolean.class.getName(); - if (type == Type.bytes()) return byte[].class.getName(); - if (type == Type.date()) return Date.class.getName(); - 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(); - if (type.getArrayElementType() == Type.bytes()) return byte[][].class.getName(); - if (type.getArrayElementType() == Type.date()) return Date[].class.getName(); - 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; + Preconditions.checkNotNull(type); + switch (type.getCode()) { + case BOOL: + return Boolean.class.getName(); + case INT64: + return Long.class.getName(); + case NUMERIC: + case PG_NUMERIC: + return BigDecimal.class.getName(); + case FLOAT64: + return Double.class.getName(); + case STRING: + case JSON: + case PG_JSONB: + return String.class.getName(); + case BYTES: + return byte[].class.getName(); + case TIMESTAMP: + return Timestamp.class.getName(); + case DATE: + return Date.class.getName(); + case ARRAY: + switch (type.getArrayElementType().getCode()) { + case BOOL: + return Boolean[].class.getName(); + case INT64: + return Long[].class.getName(); + case NUMERIC: + case PG_NUMERIC: + return BigDecimal[].class.getName(); + case FLOAT64: + return Double[].class.getName(); + case STRING: + case JSON: + case PG_JSONB: + return String[].class.getName(); + case BYTES: + return byte[][].class.getName(); + case TIMESTAMP: + return Timestamp[].class.getName(); + case DATE: + return Date[].class.getName(); + case ARRAY: + case STRUCT: + default: + // fallthrough + } + case STRUCT: + default: + return null; + } } private static final String EXPECTED_TO_STRING = @@ -472,16 +614,22 @@ private String getTypeClassName(Type type) { + "Col 5: COL5 INT64\n" + "Col 6: COL6 STRING\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 8: COL8 PG_JSONB\n" + + "Col 9: COL9 TIMESTAMP\n" + + "Col 10: COL10 NUMERIC\n" + + "Col 11: COL11 PG_NUMERIC\n" + "Col 12: COL12 ARRAY\n" + "Col 13: COL13 ARRAY\n" + "Col 14: COL14 ARRAY\n" + "Col 15: COL15 ARRAY\n" + "Col 16: COL16 ARRAY\n" - + "Col 17: CALCULATED INT64\n"; + + "Col 17: COL17 ARRAY\n" + + "Col 18: COL18 ARRAY\n" + + "Col 19: COL19 ARRAY\n" + + "Col 20: COL20 ARRAY\n" + + "Col 21: COL21 ARRAY\n" + + "Col 22: COL22 ARRAY\n" + + "Col 23: CALCULATED INT64\n"; @Test public void testToString() { diff --git a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java index ca13ea2c2b1..bbd37122cb6 100644 --- a/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java +++ b/src/test/java/com/google/cloud/spanner/jdbc/it/ITJdbcPgDatabaseMetaDataTest.java @@ -129,6 +129,7 @@ private Column( new Column( "colstringmax", Types.NVARCHAR, "character varying", null, null, null, true, null), new Column("colbytes", Types.BINARY, "bytea", 10485760, null, null, false, null), + new Column("coldate", Types.DATE, "date", 10, null, null, false, null), new Column( "coltimestamp", Types.TIMESTAMP, @@ -138,7 +139,10 @@ private Column( null, true, null), - new Column("colnumeric", Types.NUMERIC, "numeric", 15, 16383, 10, false, null)); + new Column("colnumeric", Types.NUMERIC, "numeric", 15, 16383, 10, false, null), + // TODO: Update this to typeName=jsonb and colSize=2621440 + new Column( + "coljson", Types.NVARCHAR, "character varying", null, null, null, false, null)); @Test public void testGetColumns() throws SQLException { 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 c473b6e96c2..a452fdb0a5a 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 @@ -25,6 +25,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; import com.google.cloud.ByteArray; import com.google.cloud.spanner.Database; @@ -910,6 +911,51 @@ public void test08_InsertAllColumnTypes() throws SQLException { } } + @Test + public void test08_PGInsertAllColumnTypes() throws SQLException { + assumeTrue(dialect.dialect == Dialect.POSTGRESQL); + + String sql = + "INSERT INTO TableWithAllColumnTypes (" + + "ColInt64, ColFloat64, ColBool, ColString, ColStringMax, ColBytes, ColDate, ColTimestamp, ColNumeric, ColJson" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (Connection con = createConnection(env, database)) { + 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.setDate(++index, new Date(System.currentTimeMillis())); + ps.setTimestamp(++index, new Timestamp(System.currentTimeMillis())); + ps.setBigDecimal(++index, BigDecimal.TEN); + // TODO: This test currently uses string/varchar. This should be updated to JSONB. + ps.setObject(++index, "{\"test_value\": \"foo\"}", Types.VARCHAR); + + assertEquals(1, ps.executeUpdate()); + } + try (ResultSet rs = + con.createStatement().executeQuery("SELECT * FROM TableWithAllColumnTypes")) { + int index = 0; + assertTrue(rs.next()); + 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)); + assertNotNull(rs.getDate(++index)); + assertNotNull(rs.getTimestamp(++index)); + assertEquals(BigDecimal.TEN, rs.getBigDecimal(++index)); + assertEquals("{\"test_value\": \"foo\"}", rs.getString(++index)); + + assertFalse(rs.next()); + } + } + } + @Test public void test09_MetaData_FromQuery() throws SQLException { assumeFalse( diff --git a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql index 55054231859..438d2d5df7b 100644 --- a/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql +++ b/src/test/resources/com/google/cloud/spanner/jdbc/it/CreateMusicTables_PG.sql @@ -69,8 +69,10 @@ CREATE TABLE TableWithAllColumnTypes ( ColString VARCHAR(100) NOT NULL, ColStringMax TEXT, ColBytes BYTEA NOT NULL, + ColDate DATE NOT NULL, ColTimestamp TIMESTAMP WITH TIME ZONE, - ColNumeric NUMERIC NOT NULL + ColNumeric NUMERIC NOT NULL, + ColJson VARCHAR NOT NULL ); CREATE TABLE TableWithRef (