Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jsonb data type support #926

Merged
merged 6 commits into from Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 71 additions & 34 deletions src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcArray.java
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
Expand Up @@ -281,6 +281,37 @@ public Type getSpannerType() {
return Type.json();
}
},
PG_JSONB {
@Override
public int getSqlType() {
return PgJsonbType.VENDOR_TYPE_NUMBER;
}

@Override
public Class<String> getJavaClass() {
return String.class;
}

@Override
public Code getCode() {
return Code.PG_JSONB;
}

@Override
public List<String> 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() {
Expand Down
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -490,6 +497,7 @@ private Builder setParamWithKnownType(ValueBinder<Builder> 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;
Expand All @@ -501,6 +509,9 @@ private Builder setParamWithKnownType(ValueBinder<Builder> 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) {
Expand Down Expand Up @@ -750,6 +761,8 @@ private Builder setArrayValue(ValueBinder<Builder> 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:
Expand Down Expand Up @@ -818,6 +831,8 @@ private Builder setArrayValue(ValueBinder<Builder> 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));
}
Expand Down