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: support JSON data type #447
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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<Builder> 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<Builder> binder, Object value, | |||
return null; | ||||
} | ||||
|
||||
private String getStringFromInputStream(InputStream inputStream) throws SQLException { | ||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.US_ASCII); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @zoercai There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @thiagotnunes , yep String values passed to the backend must be UTF-8 encoded There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is used for this (required JDBC interface) method: java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java Line 361 in 4ed17f0
There's also a similar Unicode version which is called setCharacterStream(..), which expects the input to be in Unicode format. The data is read as ASCII, but encoded as UTF-8 when it is sent to Cloud Spanner. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok, this method is only used in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, this is only used by the |
||||
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<Builder> binder, Object value) | ||||
throws SQLException { | ||||
|
@@ -769,14 +799,16 @@ private Builder setArrayValue(ValueBinder<Builder> binder, int type, Object valu | |||
case Types.LONGNVARCHAR: | ||||
case Types.CLOB: | ||||
case Types.NCLOB: | ||||
return binder.toStringArray((Iterable<String>) null); | ||||
return binder.toStringArray(null); | ||||
case JsonType.VENDOR_TYPE_NUMBER: | ||||
return binder.toJsonArray(null); | ||||
case Types.DATE: | ||||
return binder.toDateArray((Iterable<com.google.cloud.Date>) 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<com.google.cloud.Timestamp>) null); | ||||
return binder.toTimestampArray(null); | ||||
case Types.BINARY: | ||||
case Types.VARBINARY: | ||||
case Types.LONGVARBINARY: | ||||
|
@@ -829,7 +861,11 @@ private Builder setArrayValue(ValueBinder<Builder> 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)); | ||||
} | ||||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the way to go, but more of a design question: do we ever want to externalise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. What we could do is modify the implementation of this method: java-spanner-jdbc/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java Line 896 in 4ed17f0
It currently supports converting between common Java types, e.g. converting a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense to me. Does not necessary need to be a change added to this PR if you feel it should be out of scope here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that should be a separate change. I'll create a PR specifically for that, and once that is merged, add it for JSON here as well. |
||||
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: | ||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JDBC does not have anything like a JSON type, and we need to have a specific type code for this in order to be able to set JSON values as parameters on
PreparedStatement
. Note that this is also what is needed when working with for example PostgreSQL through JDBC: https://stackoverflow.com/questions/35844138/how-can-i-insert-json-object-into-postgres-using-java-preparedstatement