Skip to content

Commit

Permalink
feat: jsonb data type support (#926)
Browse files Browse the repository at this point in the history
Add support for the jsonb data type for PostgreSQL dialect databases.
  • Loading branch information
olavloite committed Nov 7, 2022
1 parent 5ed402e commit cefc290
Show file tree
Hide file tree
Showing 18 changed files with 644 additions and 170 deletions.
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

0 comments on commit cefc290

Please sign in to comment.