From d7ff9409e974602dc9b18f82d6dbd11d96c956bf Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 24 Aug 2021 13:24:25 +1000 Subject: [PATCH] feat: add support for JSON data type (#872) Allow users to read and write to Cloud Spanner databases using the JSON type through the client libraries. Integration tests here: https://github.com/zoercai/java-spanner/pull/1 --- .../clirr-ignored-differences.xml | 42 ++++++ .../cloud/spanner/AbstractResultSet.java | 29 ++++ .../cloud/spanner/AbstractStructReader.java | 34 +++++ .../cloud/spanner/ForwardingStructReader.java | 24 +++ .../java/com/google/cloud/spanner/Key.java | 2 + .../com/google/cloud/spanner/ResultSets.java | 20 +++ .../java/com/google/cloud/spanner/Struct.java | 14 ++ .../google/cloud/spanner/StructReader.java | 20 +++ .../java/com/google/cloud/spanner/Type.java | 12 ++ .../java/com/google/cloud/spanner/Value.java | 94 ++++++++++++ .../com/google/cloud/spanner/ValueBinder.java | 5 + .../spanner/connection/ChecksumResultSet.java | 10 ++ .../connection/DirectExecuteResultSet.java | 24 +++ .../ReplaceableForwardingResultSet.java | 24 +++ .../AbstractStructReaderTypesTest.java | 24 +++ .../cloud/spanner/GrpcResultSetTest.java | 39 +++++ .../com/google/cloud/spanner/KeyTest.java | 18 ++- .../cloud/spanner/MockSpannerServiceImpl.java | 17 +++ .../cloud/spanner/ReadFormatTestRunner.java | 6 + .../google/cloud/spanner/ResultSetsTest.java | 100 +++++++----- .../com/google/cloud/spanner/TypeTest.java | 21 +++ .../google/cloud/spanner/ValueBinderTest.java | 35 ++++- .../com/google/cloud/spanner/ValueTest.java | 142 ++++++++++++++++++ .../DirectExecuteResultSetTest.java | 9 ++ .../connection/RandomResultSetGenerator.java | 8 + .../connection/ReadWriteTransactionTest.java | 36 ++++- .../ReplaceableForwardingResultSetTest.java | 9 ++ .../google/cloud/spanner/it/ITQueryTest.java | 65 ++++++++ .../google/cloud/spanner/it/ITWriteTest.java | 90 ++++++++++- .../com/google/cloud/spanner/read_tests.json | 11 +- 30 files changed, 927 insertions(+), 57 deletions(-) diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 75df772d76..0efd90a38d 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -659,4 +659,46 @@ void setOptimizerStatisticsPackage(java.lang.String) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.lang.String getJsonInternal(int) + + + 7013 + com/google/cloud/spanner/AbstractStructReader + java.util.List getJsonListInternal(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.lang.String getJson(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.lang.String getJson(java.lang.String) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getJsonList(int) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getJsonList(java.lang.String) + + + 7013 + com/google/cloud/spanner/Value + java.lang.String getJson() + + + 7013 + com/google/cloud/spanner/Value + java.util.List getJsonArray() + + diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 09b824038e..54bfc3f545 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -378,6 +378,9 @@ private Object writeReplace() { case STRING: builder.set(fieldName).to((String) value); break; + case JSON: + builder.set(fieldName).to(Value.json((String) value)); + break; case BYTES: builder.set(fieldName).to((ByteArray) value); break; @@ -404,6 +407,9 @@ private Object writeReplace() { case STRING: builder.set(fieldName).toStringArray((Iterable) value); break; + case JSON: + builder.set(fieldName).toJsonArray((Iterable) value); + break; case BYTES: builder.set(fieldName).toBytesArray((Iterable) value); break; @@ -480,6 +486,7 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot case NUMERIC: return new BigDecimal(proto.getStringValue()); case STRING: + case JSON: checkType(fieldType, proto, KindCase.STRING_VALUE); return proto.getStringValue(); case BYTES: @@ -543,6 +550,7 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { return list; } case STRING: + case JSON: return Lists.transform( listValue.getValuesList(), input -> input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue()); @@ -654,6 +662,11 @@ protected String getStringInternal(int columnIndex) { return (String) rowData.get(columnIndex); } + @Override + protected String getJsonInternal(int columnIndex) { + return (String) rowData.get(columnIndex); + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return (ByteArray) rowData.get(columnIndex); @@ -782,6 +795,12 @@ protected List getStringListInternal(int columnIndex) { return Collections.unmodifiableList((List) rowData.get(columnIndex)); } + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getJsonListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + @Override @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getBytesListInternal(int columnIndex) { @@ -1308,6 +1327,11 @@ protected String getStringInternal(int columnIndex) { return currRow().getStringInternal(columnIndex); } + @Override + protected String getJsonInternal(int columnIndex) { + return currRow().getJsonInternal(columnIndex); + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return currRow().getBytesInternal(columnIndex); @@ -1368,6 +1392,11 @@ protected List getStringListInternal(int columnIndex) { return currRow().getStringListInternal(columnIndex); } + @Override + protected List getJsonListInternal(int columnIndex) { + return currRow().getJsonListInternal(columnIndex); + } + @Override protected List getBytesListInternal(int columnIndex) { return currRow().getBytesListInternal(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java index 9b3f1810eb..185ab03eef 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractStructReader.java @@ -43,6 +43,10 @@ public abstract class AbstractStructReader implements StructReader { protected abstract String getStringInternal(int columnIndex); + protected String getJsonInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract ByteArray getBytesInternal(int columnIndex); protected abstract Timestamp getTimestampInternal(int columnIndex); @@ -69,6 +73,10 @@ protected Value getValueInternal(int columnIndex) { protected abstract List getStringListInternal(int columnIndex); + protected List getJsonListInternal(int columnIndex) { + throw new UnsupportedOperationException("Not implemented"); + } + protected abstract List getBytesListInternal(int columnIndex); protected abstract List getTimestampListInternal(int columnIndex); @@ -162,6 +170,19 @@ public String getString(String columnName) { return getStringInternal(columnIndex); } + @Override + public String getJson(int columnIndex) { + checkNonNullOfType(columnIndex, Type.json(), columnIndex); + return getJsonInternal(columnIndex); + } + + @Override + public String getJson(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.json(), columnName); + return getJsonInternal(columnIndex); + } + @Override public ByteArray getBytes(int columnIndex) { checkNonNullOfType(columnIndex, Type.bytes(), columnIndex); @@ -317,6 +338,19 @@ public List getStringList(String columnName) { return getStringListInternal(columnIndex); } + @Override + public List getJsonList(int columnIndex) { + checkNonNullOfType(columnIndex, Type.array(Type.json()), columnIndex); + return getJsonListInternal(columnIndex); + } + + @Override + public List getJsonList(String columnName) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfType(columnIndex, Type.array(Type.json()), columnName); + return getJsonListInternal(columnIndex); + } + @Override public List getBytesList(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.bytes()), columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index d5f9488178..e225bdcc1b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -156,6 +156,18 @@ public String getString(String columnName) { return delegate.get().getString(columnName); } + @Override + public String getJson(int columnIndex) { + checkValidState(); + return delegate.get().getJson(columnIndex); + } + + @Override + public String getJson(String columnName) { + checkValidState(); + return delegate.get().getJson(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { checkValidState(); @@ -286,6 +298,18 @@ public List getStringList(String columnName) { return delegate.get().getStringList(columnName); } + @Override + public List getJsonList(int columnIndex) { + checkValidState(); + return delegate.get().getJsonList(columnIndex); + } + + @Override + public List getJsonList(String columnName) { + checkValidState(); + return delegate.get().getJsonList(columnName); + } + @Override public List getBytesList(int columnIndex) { checkValidState(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java index cf9a839585..15d4e995bf 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Key.java @@ -65,6 +65,7 @@ private Key(List parts) { *
  • {@code Float}, {@code Double} for the {@code FLOAT64} Cloud Spanner type *
  • {@code BigDecimal} for the {@code NUMERIC} Cloud Spanner type *
  • {@code String} for the {@code STRING} Cloud Spanner type + *
  • {@code String} for the {@code JSON} Cloud Spanner type *
  • {@link ByteArray} for the {@code BYTES} Cloud Spanner type *
  • {@link Timestamp} for the {@code TIMESTAMP} Cloud Spanner type *
  • {@link Date} for the {@code DATE} Cloud Spanner type @@ -228,6 +229,7 @@ public int size() { *
  • {@code FLOAT64} is represented by {@code Double} *
  • {@code NUMERIC} is represented by {@code BigDecimal} *
  • {@code STRING} is represented by {@code String} + *
  • {@code JSON} is represented by {@code String} *
  • {@code BYTES} is represented by {@link ByteArray} *
  • {@code TIMESTAMP} is represented by {@link Timestamp} *
  • {@code DATE} is represented by {@link Date} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 245e585ceb..af57b5b848 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -243,6 +243,16 @@ public String getString(String columnName) { return getCurrentRowAsStruct().getString(columnName); } + @Override + public String getJson(int columnIndex) { + return getCurrentRowAsStruct().getJson(columnIndex); + } + + @Override + public String getJson(String columnName) { + return getCurrentRowAsStruct().getJson(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { return getCurrentRowAsStruct().getBytes(columnIndex); @@ -363,6 +373,16 @@ public List getStringList(String columnName) { return getCurrentRowAsStruct().getStringList(columnName); } + @Override + public List getJsonList(int columnIndex) { + return getCurrentRowAsStruct().getJsonList(columnIndex); + } + + @Override + public List getJsonList(String columnName) { + return getCurrentRowAsStruct().getJsonList(columnName); + } + @Override public List getBytesList(int columnIndex) { return getCurrentRowAsStruct().getBytesList(columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index 3a7ddc8d99..9a437f2be3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -192,6 +192,11 @@ protected String getStringInternal(int columnIndex) { return values.get(columnIndex).getString(); } + @Override + protected String getJsonInternal(int columnIndex) { + return values.get(columnIndex).getJson(); + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return values.get(columnIndex).getBytes(); @@ -257,6 +262,11 @@ protected List getStringListInternal(int columnIndex) { return values.get(columnIndex).getStringArray(); } + @Override + protected List getJsonListInternal(int columnIndex) { + return values.get(columnIndex).getJsonArray(); + } + @Override protected List getBytesListInternal(int columnIndex) { return values.get(columnIndex).getBytesArray(); @@ -341,6 +351,8 @@ private Object getAsObject(int columnIndex) { return getBigDecimalInternal(columnIndex); case STRING: return getStringInternal(columnIndex); + case JSON: + return getJsonInternal(columnIndex); case BYTES: return getBytesInternal(columnIndex); case TIMESTAMP: @@ -361,6 +373,8 @@ private Object getAsObject(int columnIndex) { return getBigDecimalListInternal(columnIndex); case STRING: return getStringListInternal(columnIndex); + case JSON: + return getJsonListInternal(columnIndex); case BYTES: return getBytesListInternal(columnIndex); case TIMESTAMP: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java index 7b02f7078b..3779e8067d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/StructReader.java @@ -114,6 +114,16 @@ public interface StructReader { /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ String getString(String columnName); + /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ + default String getJson(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + } + + /** Returns the value of a non-{@code NULL} column with type {@link Type#string()}. */ + default String getJson(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + } + /** Returns the value of a non-{@code NULL} column with type {@link Type#bytes()}. */ ByteArray getBytes(int columnIndex); @@ -228,6 +238,16 @@ default Value getValue(String columnName) { /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ List getStringList(String columnName); + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ + default List getJsonList(int columnIndex) { + throw new UnsupportedOperationException("method should be overwritten"); + }; + + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.string())}. */ + default List getJsonList(String columnName) { + throw new UnsupportedOperationException("method should be overwritten"); + }; + /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.bytes())}. */ List getBytesList(int columnIndex); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index c02d7d4de9..4a78eca467 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -46,6 +46,7 @@ public final class Type implements Serializable { private static final Type TYPE_FLOAT64 = new Type(Code.FLOAT64, null, null); private static final Type TYPE_NUMERIC = new Type(Code.NUMERIC, null, null); private static final Type TYPE_STRING = new Type(Code.STRING, null, null); + private static final Type TYPE_JSON = new Type(Code.JSON, null, null); private static final Type TYPE_BYTES = new Type(Code.BYTES, null, null); private static final Type TYPE_TIMESTAMP = new Type(Code.TIMESTAMP, null, null); private static final Type TYPE_DATE = new Type(Code.DATE, null, null); @@ -54,6 +55,7 @@ public final class Type implements Serializable { private static final Type TYPE_ARRAY_FLOAT64 = new Type(Code.ARRAY, TYPE_FLOAT64, null); private static final Type TYPE_ARRAY_NUMERIC = new Type(Code.ARRAY, TYPE_NUMERIC, null); private static final Type TYPE_ARRAY_STRING = new Type(Code.ARRAY, TYPE_STRING, null); + private static final Type TYPE_ARRAY_JSON = new Type(Code.ARRAY, TYPE_JSON, null); private static final Type TYPE_ARRAY_BYTES = new Type(Code.ARRAY, TYPE_BYTES, null); private static final Type TYPE_ARRAY_TIMESTAMP = new Type(Code.ARRAY, TYPE_TIMESTAMP, null); private static final Type TYPE_ARRAY_DATE = new Type(Code.ARRAY, TYPE_DATE, null); @@ -94,6 +96,11 @@ public static Type string() { return TYPE_STRING; } + /** Returns the descriptor for the {@code JSON} type. */ + public static Type json() { + return TYPE_JSON; + } + /** Returns the descriptor for the {@code BYTES} type: a variable-length byte string. */ public static Type bytes() { return TYPE_BYTES; @@ -129,6 +136,8 @@ public static Type array(Type elementType) { return TYPE_ARRAY_NUMERIC; case STRING: return TYPE_ARRAY_STRING; + case JSON: + return TYPE_ARRAY_JSON; case BYTES: return TYPE_ARRAY_BYTES; case TIMESTAMP: @@ -182,6 +191,7 @@ public enum Code { NUMERIC(TypeCode.NUMERIC), FLOAT64(TypeCode.FLOAT64), STRING(TypeCode.STRING), + JSON(TypeCode.JSON), BYTES(TypeCode.BYTES), TIMESTAMP(TypeCode.TIMESTAMP), DATE(TypeCode.DATE), @@ -394,6 +404,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) { return numeric(); case STRING: return string(); + case JSON: + return json(); case BYTES: return bytes(); case TIMESTAMP: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 34fc43e751..090c636b59 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -166,6 +166,15 @@ public static Value string(@Nullable String v) { return new StringImpl(v == null, v); } + /** + * Returns a {@code STRING} value. + * + * @param v the value, which may be null + */ + public static Value json(@Nullable String v) { + return new JsonImpl(v == null, v); + } + /** * Returns a {@code BYTES} value. * @@ -339,6 +348,16 @@ public static Value stringArray(@Nullable Iterable v) { return new StringArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); } + /** + * Returns an {@code ARRAY} value. + * + * @param v the source of element values. This may be {@code null} to produce a value for which + * {@code isNull()} is {@code true}. Individual elements may also be {@code null}. + */ + public static Value jsonArray(@Nullable Iterable v) { + return new JsonArrayImpl(v == null, v == null ? null : immutableCopyOf(v)); + } + /** * Returns an {@code ARRAY} value. * @@ -441,6 +460,15 @@ private Value() {} */ public abstract String getString(); + /** + * Returns the value of a {@code JSON}-typed instance. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public String getJson() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns the value of a {@code BYTES}-typed instance. * @@ -513,6 +541,16 @@ private Value() {} */ public abstract List getStringArray(); + /** + * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself will + * never be {@code null}, elements of that list may be null. + * + * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type + */ + public List getJsonArray() { + throw new UnsupportedOperationException("Not implemented"); + } + /** * Returns the value of an {@code ARRAY}-typed instance. While the returned list itself * will never be {@code null}, elements of that list may be null. @@ -720,6 +758,11 @@ public String getString() { throw defaultGetter(Type.string()); } + @Override + public String getJson() { + throw defaultGetter(Type.json()); + } + @Override public ByteArray getBytes() { throw defaultGetter(Type.bytes()); @@ -769,6 +812,11 @@ public List getStringArray() { throw defaultGetter(Type.array(Type.string())); } + @Override + public List getJsonArray() { + throw defaultGetter(Type.array(Type.json())); + } + @Override public List getBytesArray() { throw defaultGetter(Type.array(Type.bytes())); @@ -1056,6 +1104,29 @@ void valueToString(StringBuilder b) { } } + private static class JsonImpl extends AbstractObjectValue { + + private JsonImpl(boolean isNull, @Nullable String value) { + super(isNull, Type.json(), value); + } + + @Override + public String getJson() { + checkType(Type.json()); + checkNotNull(); + return value; + } + + @Override + void valueToString(StringBuilder b) { + if (value.length() > MAX_DEBUG_STRING_LENGTH) { + b.append(value, 0, MAX_DEBUG_STRING_LENGTH - ELLIPSIS.length()).append(ELLIPSIS); + } else { + b.append(value); + } + } + } + private static class BytesImpl extends AbstractObjectValue { private BytesImpl(boolean isNull, ByteArray value) { @@ -1410,6 +1481,25 @@ void appendElement(StringBuilder b, String element) { } } + private static class JsonArrayImpl extends AbstractArrayValue { + + private JsonArrayImpl(boolean isNull, @Nullable List values) { + super(isNull, Type.json(), values); + } + + @Override + public List getJsonArray() { + checkType(getType()); + checkNotNull(); + return value; + } + + @Override + void appendElement(StringBuilder b, String element) { + b.append(element); + } + } + private static class BytesArrayImpl extends AbstractArrayValue { private BytesArrayImpl(boolean isNull, @Nullable List values) { super(isNull, Type.bytes(), values); @@ -1533,6 +1623,8 @@ private Value getValue(int fieldIndex) { return Value.int64(value.getLong(fieldIndex)); case STRING: return Value.string(value.getString(fieldIndex)); + case JSON: + return Value.json(value.getJson(fieldIndex)); case BYTES: return Value.bytes(value.getBytes(fieldIndex)); case FLOAT64: @@ -1555,6 +1647,8 @@ private Value getValue(int fieldIndex) { return Value.int64Array(value.getLongList(fieldIndex)); case STRING: return Value.stringArray(value.getStringList(fieldIndex)); + case JSON: + return Value.jsonArray(value.getJsonList(fieldIndex)); case BYTES: return Value.bytesArray(value.getBytesList(fieldIndex)); case FLOAT64: diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java index d6c5de4825..07dd246e7b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ValueBinder.java @@ -178,6 +178,11 @@ public R toStringArray(@Nullable Iterable values) { return handle(Value.stringArray(values)); } + /** Binds to {@code Value.jsonArray(values)} */ + public R toJsonArray(@Nullable Iterable values) { + return handle(Value.jsonArray(values)); + } + /** Binds to {@code Value.bytesArray(values)} */ public R toBytesArray(@Nullable Iterable values) { return handle(Value.bytesArray(values)); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index 01431f7a61..ba8f5950ae 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -242,6 +242,9 @@ public void funnel(Struct row, PrimitiveSink into) { case STRING: funnelValue(type, row.getString(i), into); break; + case JSON: + funnelValue(type, row.getJson(i), into); + break; case TIMESTAMP: funnelValue(type, row.getTimestamp(i), into); break; @@ -300,6 +303,12 @@ private void funnelArray( funnelValue(Code.STRING, value, into); } break; + case JSON: + into.putInt(row.getJsonList(columnIndex).size()); + for (String value : row.getJsonList(columnIndex)) { + funnelValue(Code.JSON, value, into); + } + break; case TIMESTAMP: into.putInt(row.getTimestampList(columnIndex).size()); for (Timestamp value : row.getTimestampList(columnIndex)) { @@ -350,6 +359,7 @@ private void funnelValue(Code type, T value, PrimitiveSink into) { into.putLong((Long) value); break; case STRING: + case JSON: String stringValue = (String) value; into.putInt(stringValue.length()); into.putUnencodedChars(stringValue); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java index f23a1fca1b..cf577cbc36 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DirectExecuteResultSet.java @@ -196,6 +196,18 @@ public String getString(String columnName) { return delegate.getString(columnName); } + @Override + public String getJson(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getJson(columnIndex); + } + + @Override + public String getJson(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getJson(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); @@ -340,6 +352,18 @@ public List getStringList(String columnName) { return delegate.getStringList(columnName); } + @Override + public List getJsonList(int columnIndex) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getJsonList(columnIndex); + } + + @Override + public List getJsonList(String columnName) { + Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); + return delegate.getJsonList(columnName); + } + @Override public List getBytesList(int columnIndex) { Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java index fac9e40f11..6bf8f046c5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSet.java @@ -196,6 +196,18 @@ public String getString(String columnName) { return delegate.getString(columnName); } + @Override + public String getJson(int columnIndex) { + checkClosed(); + return delegate.getJson(columnIndex); + } + + @Override + public String getJson(String columnName) { + checkClosed(); + return delegate.getJson(columnName); + } + @Override public ByteArray getBytes(int columnIndex) { checkClosed(); @@ -340,6 +352,18 @@ public List getStringList(String columnName) { return delegate.getStringList(columnName); } + @Override + public List getJsonList(int columnIndex) { + checkClosed(); + return delegate.getJsonList(columnIndex); + } + + @Override + public List getJsonList(String columnName) { + checkClosed(); + return delegate.getJsonList(columnName); + } + @Override public List getBytesList(int columnIndex) { checkClosed(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index 74f0a67b80..7bb8bb5194 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -70,6 +70,11 @@ protected String getStringInternal(int columnIndex) { return null; } + @Override + protected String getJsonInternal(int columnIndex) { + return null; + } + @Override protected ByteArray getBytesInternal(int columnIndex) { return null; @@ -130,6 +135,11 @@ protected List getStringListInternal(int columnIndex) { return null; } + @Override + protected List getJsonListInternal(int columnIndex) { + return null; + } + @Override protected List getBytesListInternal(int columnIndex) { return null; @@ -204,6 +214,13 @@ public static Collection parameters() { "getBytes", Collections.singletonList("getValue") }, + { + Type.json(), + "getJsonInternal", + "{\"color\":\"red\",\"value\":\"#f00\"}", + "getJson", + Collections.singletonList("getValue") + }, { Type.timestamp(), "getTimestampInternal", @@ -274,6 +291,13 @@ public static Collection parameters() { "getStringList", Collections.singletonList("getValue") }, + { + Type.array(Type.json()), + "getJsonListInternal", + Arrays.asList("{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"), + "getJsonList", + Collections.singletonList("getValue") + }, { Type.array(Type.bytes()), "getBytesListInternal", diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 3bcd10e292..a11ac78b54 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -696,6 +697,25 @@ public void getTimestamp() { .isEqualTo(Timestamp.parseTimestamp("0001-01-01T00:00:00Z")); } + @Test + public void getJson() { + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.json())))) + .addValues(Value.json("{\"color\":\"red\",\"value\":\"#f00\"}").toProto()) + .addValues(Value.json("{}").toProto()) + .addValues(Value.json("[]").toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals("{\"color\":\"red\",\"value\":\"#f00\"}", resultSet.getJson(0)); + assertTrue(resultSet.next()); + assertEquals("{}", resultSet.getJson(0)); + assertTrue(resultSet.next()); + assertEquals("[]", resultSet.getJson(0)); + } + @Test public void getBooleanArray() { boolean[] boolArray = {true, true, false}; @@ -799,4 +819,23 @@ public void getDateList() { assertThat(resultSet.next()).isTrue(); assertThat(resultSet.getDateList(0)).isEqualTo(dateList); } + + @Test + public void getJsonList() { + List jsonList = new ArrayList<>(); + jsonList.add("{\"color\":\"red\",\"value\":\"#f00\"}"); + jsonList.add("{\"special\":\"%😃∮πρότερονแผ่นดินฮั่นเสื่อมሰማይᚻᛖ\"}"); + jsonList.add("[]"); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.array(Type.json()))))) + .addValues(Value.jsonArray(jsonList).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals(jsonList, resultSet.getJsonList(0)); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java index e88d3b951c..47aca8e18e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java @@ -51,6 +51,7 @@ public void of() { String numeric = "3.141592"; String timestamp = "2015-09-15T00:00:00Z"; String date = "2015-09-15"; + String json = "{\"color\":\"red\",\"value\":\"#f00\"}"; k = Key.of( null, @@ -61,10 +62,11 @@ public void of() { 4.0d, new BigDecimal(numeric), "x", + json, ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), Date.parseDate(date)); - assertThat(k.size()).isEqualTo(11); + assertThat(k.size()).isEqualTo(12); assertThat(k.getParts()) .containsExactly( null, @@ -75,6 +77,7 @@ public void of() { 4.0d, BigDecimal.valueOf(3141592, 6), "x", + json, ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), Date.parseDate(date)) @@ -91,6 +94,7 @@ public void builder() { String numeric = "3.141592"; String timestamp = "2015-09-15T00:00:00Z"; String date = "2015-09-15"; + String json = "{\"color\":\"red\",\"value\":\"#f00\"}"; Key k = Key.newBuilder() .append((Boolean) null) @@ -101,11 +105,12 @@ public void builder() { .append(4.0d) .append(new BigDecimal(numeric)) .append("x") + .append(json) .append(ByteArray.copyFrom("y")) .append(Timestamp.parseTimestamp(timestamp)) .append(Date.parseDate(date)) .build(); - assertThat(k.size()).isEqualTo(11); + assertThat(k.size()).isEqualTo(12); assertThat(k.getParts()) .containsExactly( null, @@ -116,6 +121,7 @@ public void builder() { 4.0d, BigDecimal.valueOf(3141592, 6), "x", + json, ByteArray.copyFrom("y"), Timestamp.parseTimestamp(timestamp), Date.parseDate(date)) @@ -138,6 +144,8 @@ public void testToString() { assertThat(Key.of(2.0).toString()).isEqualTo("[2.0]"); assertThat(Key.of(new BigDecimal("3.14")).toString()).isEqualTo("[3.14]"); assertThat(Key.of("xyz").toString()).isEqualTo("[xyz]"); + assertThat(Key.of("{\"color\":\"red\",\"value\":\"#f00\"}").toString()) + .isEqualTo("[{\"color\":\"red\",\"value\":\"#f00\"}]"); ByteArray b = ByteArray.copyFrom("xyz"); assertThat(Key.of(b).toString()).isEqualTo("[" + b.toString() + "]"); String timestamp = "2015-09-15T00:00:00Z"; @@ -182,6 +190,9 @@ public void equalsAndHashCode() { tester.addEqualityGroup(Key.of("a", "b", "c")); tester.addEqualityGroup( Key.of(ByteArray.copyFrom("a")), Key.newBuilder().append(ByteArray.copyFrom("a")).build()); + tester.addEqualityGroup( + Key.of("{\"color\":\"red\",\"value\":\"#f00\"}"), + Key.newBuilder().append("{\"color\":\"red\",\"value\":\"#f00\"}").build()); Timestamp t = Timestamp.parseTimestamp("2015-09-15T00:00:00Z"); tester.addEqualityGroup(Key.of(t), Key.newBuilder().append(t).build()); Date d = Date.parseDate("2016-09-15"); @@ -200,6 +211,7 @@ public void serialization() { reserializeAndAssert(Key.of(2.0)); reserializeAndAssert(Key.of(new BigDecimal("3.141592"))); reserializeAndAssert(Key.of("xyz")); + reserializeAndAssert(Key.of("{\"color\":\"red\",\"value\":\"#f00\"}")); reserializeAndAssert(Key.of(ByteArray.copyFrom("xyz"))); reserializeAndAssert(Key.of(Timestamp.parseTimestamp("2015-09-15T00:00:00Z"))); reserializeAndAssert(Key.of(Date.parseDate("2015-09-15"))); @@ -220,6 +232,7 @@ public void toProto() { .append(4.0d) .append(new BigDecimal("6.62607004e-34")) .append("x") + .append("{\"color\":\"red\",\"value\":\"#f00\"}") .append(ByteArray.copyFrom("y")) .append(Timestamp.parseTimestamp(timestamp)) .append(Date.parseDate(date)) @@ -233,6 +246,7 @@ public void toProto() { builder.addValuesBuilder().setNumberValue(4.0d); builder.addValuesBuilder().setStringValue("6.62607004E-34"); builder.addValuesBuilder().setStringValue("x"); + builder.addValuesBuilder().setStringValue("{\"color\":\"red\",\"value\":\"#f00\"}"); builder.addValuesBuilder().setStringValue("eQ=="); builder.addValuesBuilder().setStringValue(timestamp); builder.addValuesBuilder().setStringValue(date); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 39fe74566a..7023a8c20f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -1202,6 +1202,9 @@ private Statement buildStatement( case TIMESTAMP: builder.bind(entry.getKey()).toTimestampArray(null); break; + case JSON: + builder.bind(entry.getKey()).toJsonArray(null); + break; case STRUCT: case TYPE_CODE_UNSPECIFIED: case UNRECOGNIZED: @@ -1235,6 +1238,9 @@ private Statement buildStatement( case TIMESTAMP: builder.bind(entry.getKey()).to((com.google.cloud.Timestamp) null); break; + case JSON: + builder.bind(entry.getKey()).to(Value.json((String) null)); + break; case TYPE_CODE_UNSPECIFIED: case UNRECOGNIZED: default: @@ -1301,6 +1307,14 @@ private Statement buildStatement( GrpcStruct.decodeArrayValue( com.google.cloud.spanner.Type.timestamp(), value.getListValue())); break; + case JSON: + builder + .bind(entry.getKey()) + .toJsonArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.json(), value.getListValue())); + break; case STRUCT: case TYPE_CODE_UNSPECIFIED: case UNRECOGNIZED: @@ -1335,6 +1349,9 @@ private Statement buildStatement( .bind(entry.getKey()) .to(com.google.cloud.Timestamp.parseTimestamp(value.getStringValue())); break; + case JSON: + builder.bind(entry.getKey()).to(Value.json(value.getStringValue())); + break; case TYPE_CODE_UNSPECIFIED: case UNRECOGNIZED: default: diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java index 8d93971229..d8d39e6931 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java @@ -158,6 +158,9 @@ private void assertRow(Struct actualRow, JSONArray expectedRow) throws Exception case STRING: assertThat(actualRow.getString(i)).isEqualTo(expectedRow.getString(i)); break; + case JSON: + assertThat(actualRow.getJson(i)).isEqualTo(expectedRow.getString(i)); + break; case INT64: assertThat(actualRow.getLong(i)).isEqualTo(expectedRow.getLong(i)); break; @@ -190,6 +193,9 @@ private List getRawList(Struct actualRow, int index, Type elementType) { case STRING: rawList = actualRow.getStringList(index); break; + case JSON: + rawList = actualRow.getJsonList(index); + break; case BYTES: rawList = actualRow.getBytesList(index); break; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 7f36d8bad7..85cdc0f687 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -27,7 +27,6 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; -import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.MoreExecutors; @@ -51,6 +50,7 @@ public void resultSetIteration() { double doubleVal = 1.2; BigDecimal bigDecimalVal = BigDecimal.valueOf(123, 2); String stringVal = "stringVal"; + String jsonVal = "{\"color\":\"red\",\"value\":\"#f00\"}"; String byteVal = "101"; long usecs = 32343; int year = 2018; @@ -78,6 +78,7 @@ public void resultSetIteration() { Date.fromYearMonthDay(1, 2, 3), Date.fromYearMonthDay(4, 5, 6), Date.fromYearMonthDay(7, 8, 9) }; String[] stringArray = {"abc", "def", "ghi"}; + String[] jsonArray = {"{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"}; Type type = Type.struct( @@ -87,6 +88,7 @@ public void resultSetIteration() { Type.StructField.of("doubleVal", Type.float64()), Type.StructField.of("bigDecimalVal", Type.numeric()), Type.StructField.of("stringVal", Type.string()), + Type.StructField.of("jsonVal", Type.json()), Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), @@ -97,7 +99,8 @@ public void resultSetIteration() { Type.StructField.of("byteArray", Type.array(Type.bytes())), Type.StructField.of("timestampArray", Type.array(Type.timestamp())), Type.StructField.of("dateArray", Type.array(Type.date())), - Type.StructField.of("stringArray", Type.array(Type.string()))); + Type.StructField.of("stringArray", Type.array(Type.string())), + Type.StructField.of("jsonArray", Type.array(Type.json()))); Struct struct1 = Struct.newBuilder() .set("f1") @@ -112,6 +115,8 @@ public void resultSetIteration() { .to(Value.numeric(bigDecimalVal)) .set("stringVal") .to(stringVal) + .set("jsonVal") + .to(Value.json(jsonVal)) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom(byteVal))) .set("timestamp") @@ -134,6 +139,8 @@ public void resultSetIteration() { .to(Value.dateArray(Arrays.asList(dateArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) + .set("jsonArray") + .to(Value.jsonArray(Arrays.asList(jsonArray))) .build(); Struct struct2 = Struct.newBuilder() @@ -149,6 +156,8 @@ public void resultSetIteration() { .to(Value.numeric(bigDecimalVal)) .set("stringVal") .to(stringVal) + .set("jsonVal") + .to(Value.json(jsonVal)) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom(byteVal))) .set("timestamp") @@ -171,12 +180,15 @@ public void resultSetIteration() { .to(Value.dateArray(Arrays.asList(dateArray))) .set("stringArray") .to(Value.stringArray(Arrays.asList(stringArray))) + .set("jsonArray") + .to(Value.jsonArray(Arrays.asList(jsonArray))) .build(); ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2)); IllegalStateException e = assertThrows(IllegalStateException.class, () -> rs.getType()); assertThat(e.getMessage()).contains("Must be preceded by a next() call"); + int columnIndex = 0; assertThat(rs.next()).isTrue(); assertThat(rs.getType()).isEqualTo(type); assertThat(rs.getColumnCount()).isEqualTo(type.getStructFields().size()); @@ -191,82 +203,90 @@ public void resultSetIteration() { assertThat(rs.getColumnType("f3")).isEqualTo(Type.bool()); assertThat(rs.getColumnType(2)).isEqualTo(Type.bool()); assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct1); - assertThat(rs.getString(0)).isEqualTo("x"); - assertThat(rs.getValue(0)).isEqualTo(Value.string("x")); - assertThat(rs.getLong(1)).isEqualTo(2L); - assertThat(rs.getValue(1)).isEqualTo(Value.int64(2L)); - assertThat(rs.getBoolean(2)).isTrue(); - assertThat(rs.getValue(2)).isEqualTo(Value.bool(true)); + assertThat(rs.getString(columnIndex)).isEqualTo("x"); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.string("x")); + assertThat(rs.getLong(columnIndex)).isEqualTo(2L); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.int64(2L)); + assertThat(rs.getBoolean(columnIndex)).isTrue(); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.bool(true)); assertThat(rs.getBoolean("f3")).isTrue(); assertThat(rs.getValue("f3")).isEqualTo(Value.bool(true)); assertThat(rs.getDouble("doubleVal")).isWithin(0.0).of(doubleVal); assertThat(rs.getValue("doubleVal").getFloat64()).isWithin(0.0).of(doubleVal); - assertThat(rs.getDouble(3)).isWithin(0.0).of(doubleVal); - assertThat(rs.getValue(3).getFloat64()).isWithin(0.0).of(doubleVal); + assertThat(rs.getDouble(columnIndex)).isWithin(0.0).of(doubleVal); + assertThat(rs.getValue(columnIndex++).getFloat64()).isWithin(0.0).of(doubleVal); assertThat(rs.getBigDecimal("bigDecimalVal")).isEqualTo(new BigDecimal("1.23")); assertThat(rs.getValue("bigDecimalVal")).isEqualTo(Value.numeric(new BigDecimal("1.23"))); - assertThat(rs.getBigDecimal(4)).isEqualTo(new BigDecimal("1.23")); - assertThat(rs.getValue(4)).isEqualTo(Value.numeric(new BigDecimal("1.23"))); - assertThat(rs.getString(5)).isEqualTo(stringVal); - assertThat(rs.getValue(5)).isEqualTo(Value.string(stringVal)); + assertThat(rs.getBigDecimal(columnIndex)).isEqualTo(new BigDecimal("1.23")); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.numeric(new BigDecimal("1.23"))); + assertThat(rs.getString(columnIndex)).isEqualTo(stringVal); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.string(stringVal)); assertThat(rs.getString("stringVal")).isEqualTo(stringVal); assertThat(rs.getValue("stringVal")).isEqualTo(Value.string(stringVal)); - assertThat(rs.getBytes(6)).isEqualTo(ByteArray.copyFrom(byteVal)); - assertThat(rs.getValue(6)).isEqualTo(Value.bytes(ByteArray.copyFrom(byteVal))); + assertThat(rs.getJson(columnIndex)).isEqualTo(jsonVal); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.json(jsonVal)); + assertThat(rs.getJson("jsonVal")).isEqualTo(jsonVal); + assertThat(rs.getValue("jsonVal")).isEqualTo(Value.json(jsonVal)); + assertThat(rs.getBytes(columnIndex)).isEqualTo(ByteArray.copyFrom(byteVal)); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.bytes(ByteArray.copyFrom(byteVal))); assertThat(rs.getBytes("byteVal")).isEqualTo(ByteArray.copyFrom(byteVal)); assertThat(rs.getValue("byteVal")).isEqualTo(Value.bytes(ByteArray.copyFrom(byteVal))); - assertThat(rs.getTimestamp(7)).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); - assertThat(rs.getValue(7)).isEqualTo(Value.timestamp(Timestamp.ofTimeMicroseconds(usecs))); + assertThat(rs.getTimestamp(columnIndex)).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo(Value.timestamp(Timestamp.ofTimeMicroseconds(usecs))); assertThat(rs.getTimestamp("timestamp")).isEqualTo(Timestamp.ofTimeMicroseconds(usecs)); assertThat(rs.getValue("timestamp")) .isEqualTo(Value.timestamp(Timestamp.ofTimeMicroseconds(usecs))); - assertThat(rs.getDate(8)).isEqualTo(Date.fromYearMonthDay(year, month, day)); - assertThat(rs.getValue(8)).isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); + assertThat(rs.getDate(columnIndex)).isEqualTo(Date.fromYearMonthDay(year, month, day)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); assertThat(rs.getDate("date")).isEqualTo(Date.fromYearMonthDay(year, month, day)); assertThat(rs.getValue("date")).isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); - assertThat(rs.getBooleanArray(9)).isEqualTo(boolArray); - assertThat(rs.getValue(9)).isEqualTo(Value.boolArray(boolArray)); + assertThat(rs.getBooleanArray(columnIndex)).isEqualTo(boolArray); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.boolArray(boolArray)); assertThat(rs.getBooleanArray("boolArray")).isEqualTo(boolArray); assertThat(rs.getValue("boolArray")).isEqualTo(Value.boolArray(boolArray)); - assertThat(rs.getBooleanList(9)).isEqualTo(Booleans.asList(boolArray)); - assertThat(rs.getBooleanList("boolArray")).isEqualTo(Booleans.asList(boolArray)); - assertThat(rs.getLongArray(10)).isEqualTo(longArray); - assertThat(rs.getValue(10)).isEqualTo(Value.int64Array(longArray)); + assertThat(rs.getLongArray(columnIndex)).isEqualTo(longArray); + assertThat(rs.getValue(columnIndex)).isEqualTo(Value.int64Array(longArray)); assertThat(rs.getLongArray("longArray")).isEqualTo(longArray); assertThat(rs.getValue("longArray")).isEqualTo(Value.int64Array(longArray)); - assertThat(rs.getLongList(10)).isEqualTo(Longs.asList(longArray)); + assertThat(rs.getLongList(columnIndex++)).isEqualTo(Longs.asList(longArray)); assertThat(rs.getLongList("longArray")).isEqualTo(Longs.asList(longArray)); - assertThat(rs.getDoubleArray(11)).usingTolerance(0.0).containsAtLeast(doubleArray); - assertThat(rs.getValue(11)).isEqualTo(Value.float64Array(doubleArray)); + assertThat(rs.getDoubleArray(columnIndex)).usingTolerance(0.0).containsAtLeast(doubleArray); + assertThat(rs.getValue(columnIndex)).isEqualTo(Value.float64Array(doubleArray)); assertThat(rs.getDoubleArray("doubleArray")) .usingTolerance(0.0) .containsExactly(doubleArray) .inOrder(); assertThat(rs.getValue("doubleArray")).isEqualTo(Value.float64Array(doubleArray)); - assertThat(rs.getDoubleList(11)).isEqualTo(Doubles.asList(doubleArray)); + assertThat(rs.getDoubleList(columnIndex++)).isEqualTo(Doubles.asList(doubleArray)); assertThat(rs.getDoubleList("doubleArray")).isEqualTo(Doubles.asList(doubleArray)); - assertThat(rs.getBigDecimalList(12)).isEqualTo(Arrays.asList(bigDecimalArray)); - assertThat(rs.getValue(12)).isEqualTo(Value.numericArray(Arrays.asList(bigDecimalArray))); + assertThat(rs.getBigDecimalList(columnIndex)).isEqualTo(Arrays.asList(bigDecimalArray)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo(Value.numericArray(Arrays.asList(bigDecimalArray))); assertThat(rs.getBigDecimalList("bigDecimalArray")).isEqualTo(Arrays.asList(bigDecimalArray)); assertThat(rs.getValue("bigDecimalArray")) .isEqualTo(Value.numericArray(Arrays.asList(bigDecimalArray))); - assertThat(rs.getBytesList(13)).isEqualTo(Arrays.asList(byteArray)); - assertThat(rs.getValue(13)).isEqualTo(Value.bytesArray(Arrays.asList(byteArray))); + assertThat(rs.getBytesList(columnIndex)).isEqualTo(Arrays.asList(byteArray)); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.bytesArray(Arrays.asList(byteArray))); assertThat(rs.getBytesList("byteArray")).isEqualTo(Arrays.asList(byteArray)); assertThat(rs.getValue("byteArray")).isEqualTo(Value.bytesArray(Arrays.asList(byteArray))); - assertThat(rs.getTimestampList(14)).isEqualTo(Arrays.asList(timestampArray)); - assertThat(rs.getValue(14)).isEqualTo(Value.timestampArray(Arrays.asList(timestampArray))); + assertThat(rs.getTimestampList(columnIndex)).isEqualTo(Arrays.asList(timestampArray)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo(Value.timestampArray(Arrays.asList(timestampArray))); assertThat(rs.getTimestampList("timestampArray")).isEqualTo(Arrays.asList(timestampArray)); assertThat(rs.getValue("timestampArray")) .isEqualTo(Value.timestampArray(Arrays.asList(timestampArray))); - assertThat(rs.getDateList(15)).isEqualTo(Arrays.asList(dateArray)); - assertThat(rs.getValue(15)).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); + assertThat(rs.getDateList(columnIndex)).isEqualTo(Arrays.asList(dateArray)); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); assertThat(rs.getDateList("dateArray")).isEqualTo(Arrays.asList(dateArray)); assertThat(rs.getValue("dateArray")).isEqualTo(Value.dateArray(Arrays.asList(dateArray))); - assertThat(rs.getStringList(16)).isEqualTo(Arrays.asList(stringArray)); - assertThat(rs.getValue(16)).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); + assertThat(rs.getStringList(columnIndex)).isEqualTo(Arrays.asList(stringArray)); + assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); assertThat(rs.getStringList("stringArray")).isEqualTo(Arrays.asList(stringArray)); assertThat(rs.getValue("stringArray")).isEqualTo(Value.stringArray(Arrays.asList(stringArray))); + assertThat(rs.getJsonList(columnIndex)).isEqualTo(Arrays.asList(jsonArray)); + assertThat(rs.getJsonList("jsonArray")).isEqualTo(Arrays.asList(jsonArray)); assertThat(rs.next()).isTrue(); assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct2); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index b08545af49..afbf273bf8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; +import com.google.cloud.spanner.Type.Code; import com.google.spanner.v1.TypeCode; import org.hamcrest.MatcherAssert; import org.junit.Test; @@ -114,6 +115,16 @@ Type newType() { }.test(); } + @Test + public void json() { + new ScalarTypeTester(Code.JSON, TypeCode.JSON) { + @Override + Type newType() { + return Type.json(); + } + }.test(); + } + @Test public void bytes() { new ScalarTypeTester(Type.Code.BYTES, TypeCode.BYTES) { @@ -233,6 +244,16 @@ Type newElementType() { }.test(); } + @Test + public void jsonArray() { + new ArrayTypeTester(Code.JSON, TypeCode.JSON, true) { + @Override + Type newElementType() { + return Type.json(); + } + }.test(); + } + @Test public void bytesArray() { new ArrayTypeTester(Type.Code.BYTES, TypeCode.BYTES, true) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java index d171a46acb..bc1674e5a0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueBinderTest.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultJson; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -35,6 +36,8 @@ /** Unit tests for {@link com.google.cloud.spanner.ValueBinder}. */ @RunWith(JUnit4.class) public class ValueBinderTest { + private static final String JSON_METHOD_NAME = "json"; + private Value lastValue; private int lastReturnValue; private ValueBinder binder = new BinderImpl(); @@ -48,7 +51,8 @@ Integer handle(Value value) { } @Test - public void reflection() throws InvocationTargetException, IllegalAccessException { + public void reflection() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { // Test that every Value factory method has a counterpart in ValueBinder, and that invoking it // produces the expected Value. for (Method method : Value.class.getMethods()) { @@ -111,17 +115,32 @@ public void reflection() throws InvocationTargetException, IllegalAccessExceptio } else if (binderMethod.getParameterTypes().length == 1) { // Test unary null. if (!binderMethod.getParameterTypes()[0].isPrimitive()) { + if (method.getName().equalsIgnoreCase(JSON_METHOD_NAME)) { + // Special case for json to change the method from ValueBinder.to(String) to + // ValueBinder.to(Value) + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.json(null))).isEqualTo(lastReturnValue); + } else { + assertThat(binderMethod.invoke(binder, (Object) null)).isEqualTo(lastReturnValue); + } Value expected = (Value) method.invoke(Value.class, (Object) null); - assertThat(binderMethod.invoke(binder, (Object) null)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); assertThat(binder.to(expected)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); } // Test unary non-null. - Object defaultObject = DefaultValues.getDefault(method.getGenericParameterTypes()[0]); + Object defaultObject; + if (method.getName().equalsIgnoreCase(JSON_METHOD_NAME)) { + defaultObject = defaultJson(); + binderMethod = ValueBinder.class.getMethod("to", Value.class); + assertThat(binderMethod.invoke(binder, Value.json(defaultJson()))) + .isEqualTo(lastReturnValue); + } else { + defaultObject = DefaultValues.getDefault(method.getGenericParameterTypes()[0]); + assertThat(binderMethod.invoke(binder, defaultObject)).isEqualTo(lastReturnValue); + } Value expected = (Value) method.invoke(Value.class, defaultObject); - assertThat(binderMethod.invoke(binder, defaultObject)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); assertThat(binder.to(expected)).isEqualTo(lastReturnValue); @@ -195,6 +214,10 @@ public static String defaultString() { return "x"; } + public static String defaultJson() { + return "{\"color\":\"red\",\"value\":\"#f00\"}"; + } + public static ByteArray defaultByteArray() { return ByteArray.copyFrom(new byte[] {'x'}); } @@ -239,6 +262,10 @@ public static Iterable defaultStringIterable() { return Arrays.asList("a", "b"); } + public static Iterable defaultJsonIterable() { + return Arrays.asList("{}", "[]", "{\"color\":\"red\",\"value\":\"#f00\"}"); + } + public static Iterable defaultByteArrayIterable() { return Arrays.asList(ByteArray.copyFrom("x"), ByteArray.copyFrom("y")); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index cf7a1f0ffb..7c2d7d0a95 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -20,7 +20,10 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -356,6 +359,59 @@ public void stringLong() { assertThat(v.toString()).endsWith("..."); } + @Test + public void json() { + String json = "{\"color\":\"red\",\"value\":\"#f00\"}"; + Value v = Value.json(json); + assertEquals(Type.json(), v.getType()); + assertFalse(v.isNull()); + assertEquals(json, v.getJson()); + } + + @Test + public void jsonNull() { + Value v = Value.json(null); + assertEquals(Type.json(), v.getType()); + assertTrue(v.isNull()); + assertEquals(NULL_STRING, v.toString()); + try { + v.getJson(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } + } + + @Test + public void jsonEmpty() { + String json = "{}"; + Value v = Value.json(json); + assertEquals(json, v.getJson()); + } + + @Test + public void jsonWithEmptyArray() { + String json = "[]"; + Value v = Value.json(json); + assertEquals(json, v.getJson()); + } + + @Test + public void jsonWithArray() { + String json = + "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; + Value v = Value.json(json); + assertEquals(json, v.getJson()); + } + + @Test + public void jsonNested() { + String json = + "[{\"id\":\"0001\",\"type\":\"donut\",\"name\":\"Cake\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"},{\"id\":\"1003\",\"type\":\"Blueberry\"},{\"id\":\"1004\",\"type\":\"Devil's Food\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5007\",\"type\":\"Powdered Sugar\"},{\"id\":\"5006\",\"type\":\"Chocolate with Sprinkles\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0002\",\"type\":\"donut\",\"name\":\"Raised\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5005\",\"type\":\"Sugar\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]},{\"id\":\"0003\",\"type\":\"donut\",\"name\":\"Old Fashioned\",\"ppu\":0.55,\"batters\":{\"batter\":[{\"id\":\"1001\",\"type\":\"Regular\"},{\"id\":\"1002\",\"type\":\"Chocolate\"}]},\"topping\":[{\"id\":\"5001\",\"type\":\"None\"},{\"id\":\"5002\",\"type\":\"Glazed\"},{\"id\":\"5003\",\"type\":\"Chocolate\"},{\"id\":\"5004\",\"type\":\"Maple\"}]}]"; + Value v = Value.json(json); + assertEquals(json, v.getJson()); + } + @Test public void bytes() { ByteArray bytes = newByteArray("abc"); @@ -668,6 +724,52 @@ public void stringArrayTryGetBytesArray() { assertThat(e.getMessage()).contains("Expected: ARRAY actual: ARRAY"); } + @Test + public void jsonArray() { + String one = "{}"; + String two = null; + String three = "{\"color\":\"red\",\"value\":\"#f00\"}"; + Value v = Value.jsonArray(Arrays.asList(one, two, three)); + assertFalse(v.isNull()); + assertThat(v.getJsonArray()).containsExactly(one, two, three).inOrder(); + assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.toString()); + } + + @Test + public void jsonArrayNull() { + Value v = Value.jsonArray(null); + assertTrue(v.isNull()); + assertEquals(NULL_STRING, v.toString()); + try { + v.getJsonArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } + } + + @Test + public void jsonArrayTryGetBytesArray() { + Value value = Value.jsonArray(Arrays.asList("{}")); + try { + value.getBytesArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } + } + + @Test + public void jsonArrayTryGetStringArray() { + Value value = Value.jsonArray(Arrays.asList("{}")); + try { + value.getStringArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } + } + @Test public void bytesArray() { ByteArray a = newByteArray("a"); @@ -921,6 +1023,13 @@ public void testValueToProto() { com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), Value.string(null).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setStringValue("{}").build(), + Value.json("{}").toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(), + Value.json(null).toProto()); + assertEquals( com.google.protobuf.Value.newBuilder() .setStringValue(ByteArray.copyFrom("test").toBase64()) @@ -1003,6 +1112,18 @@ public void testValueToProto() { .build()))) .build(), Value.stringArray(Arrays.asList("test", null)).toProto()); + assertEquals( + com.google.protobuf.Value.newBuilder() + .setListValue( + ListValue.newBuilder() + .addAllValues( + Arrays.asList( + com.google.protobuf.Value.newBuilder().setStringValue("{}").build(), + com.google.protobuf.Value.newBuilder() + .setNullValue(NullValue.NULL_VALUE) + .build()))) + .build(), + Value.jsonArray(Arrays.asList("{}", null)).toProto()); assertEquals( com.google.protobuf.Value.newBuilder() .setListValue( @@ -1264,6 +1385,8 @@ public void testValueToProto() { @Test public void testEqualsHashCode() { EqualsTester tester = new EqualsTester(); + String emptyJson = "{}"; + String simpleJson = "{\"color\":\"red\",\"value\":\"#f00\"}"; tester.addEqualityGroup(Value.bool(true), Value.bool(Boolean.TRUE)); tester.addEqualityGroup(Value.bool(false)); @@ -1286,6 +1409,11 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.string("def")); tester.addEqualityGroup(Value.string(null)); + tester.addEqualityGroup(Value.json(simpleJson), Value.json(simpleJson)); + tester.addEqualityGroup(Value.json("{}")); + tester.addEqualityGroup(Value.json("[]")); + tester.addEqualityGroup(Value.json(null)); + tester.addEqualityGroup(Value.bytes(newByteArray("abc")), Value.bytes(newByteArray("abc"))); tester.addEqualityGroup(Value.bytes(newByteArray("def"))); tester.addEqualityGroup(Value.bytes(null)); @@ -1348,6 +1476,12 @@ public void testEqualsHashCode() { tester.addEqualityGroup(Value.stringArray(Collections.singletonList("c"))); tester.addEqualityGroup(Value.stringArray(null)); + tester.addEqualityGroup( + Value.jsonArray(Arrays.asList(emptyJson, simpleJson)), + Value.jsonArray(Arrays.asList(emptyJson, simpleJson))); + tester.addEqualityGroup(Value.jsonArray(Arrays.asList("[]"))); + tester.addEqualityGroup(Value.jsonArray(null)); + tester.addEqualityGroup( Value.bytesArray(Arrays.asList(newByteArray("a"), newByteArray("b"))), Value.bytesArray(Arrays.asList(newByteArray("a"), newByteArray("b")))); @@ -1398,6 +1532,9 @@ public void serialization() { reserializeAndAssert(Value.string("abc")); reserializeAndAssert(Value.string(null)); + reserializeAndAssert(Value.json("{\"color\":\"red\",\"value\":\"#f00\"}")); + reserializeAndAssert(Value.json(null)); + reserializeAndAssert(Value.bytes(newByteArray("abc"))); reserializeAndAssert(Value.bytes(null)); @@ -1445,6 +1582,11 @@ public void serialization() { reserializeAndAssert(Value.stringArray(of)); reserializeAndAssert(Value.stringArray(null)); + BrokenSerializationList json = + BrokenSerializationList.of("{}", "{\"color\":\"red\",\"value\":\"#f00\"}"); + reserializeAndAssert(Value.jsonArray(json)); + reserializeAndAssert(Value.jsonArray(null)); + reserializeAndAssert( Value.bytesArray(BrokenSerializationList.of(newByteArray("a"), newByteArray("b")))); reserializeAndAssert(Value.bytesArray(null)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java index 9695794805..7145ccdf74 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java @@ -230,6 +230,15 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getStringList("test2"); verify(delegate).getStringList("test2"); + subject.getJson(0); + verify(delegate).getJson(0); + subject.getJson("test0"); + verify(delegate).getJson("test0"); + subject.getJsonList(2); + verify(delegate).getJsonList(2); + subject.getJsonList("test2"); + verify(delegate).getJsonList("test2"); + subject.getStructList(0); subject.getStructList("test0"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java index 2cda94baad..a2870461cd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/RandomResultSetGenerator.java @@ -44,6 +44,7 @@ public class RandomResultSetGenerator { Type.newBuilder().setCode(TypeCode.FLOAT64).build(), Type.newBuilder().setCode(TypeCode.NUMERIC).build(), Type.newBuilder().setCode(TypeCode.STRING).build(), + Type.newBuilder().setCode(TypeCode.JSON).build(), Type.newBuilder().setCode(TypeCode.BYTES).build(), Type.newBuilder().setCode(TypeCode.DATE).build(), Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(), @@ -67,6 +68,10 @@ public class RandomResultSetGenerator { .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.JSON)) + .build(), Type.newBuilder() .setCode(TypeCode.ARRAY) .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES)) @@ -139,6 +144,9 @@ private void setRandomValue(Value.Builder builder, Type type) { random.nextBytes(bytes); builder.setStringValue(Base64.encodeBase64String(bytes)); break; + case JSON: + builder.setStringValue("\"" + random.nextInt(200) + "\":\"" + random.nextInt(200) + "\""); + break; case DATE: Date date = Date.fromYearMonthDay( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 1d74a209c5..204a14756f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -49,6 +49,7 @@ import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.Value; import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.cloud.spanner.connection.StatementParser.StatementType; import com.google.rpc.RetryInfo; @@ -493,12 +494,18 @@ public void testChecksumResultSet() { ParsedStatement parsedStatement = mock(ParsedStatement.class); Statement statement = Statement.of("SELECT * FROM FOO"); when(parsedStatement.getStatement()).thenReturn(statement); + + String arrayJson = + "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; + String emptyArrayJson = "[]"; + String simpleJson = "{\"color\":\"red\",\"value\":\"#f00\"}"; ResultSet delegate1 = ResultSets.forRows( Type.struct( StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), - StructField.of("AMOUNT", Type.numeric())), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json())), Arrays.asList( Struct.newBuilder() .set("ID") @@ -507,6 +514,8 @@ public void testChecksumResultSet() { .to("TEST 1") .set("AMOUNT") .to(BigDecimal.valueOf(550, 2)) + .set("JSON") + .to(Value.json(simpleJson)) .build(), Struct.newBuilder() .set("ID") @@ -515,6 +524,8 @@ public void testChecksumResultSet() { .to("TEST 2") .set("AMOUNT") .to(BigDecimal.valueOf(750, 2)) + .set("JSON") + .to(Value.json(arrayJson)) .build())); ChecksumResultSet rs1 = transaction.createChecksumResultSet(delegate1, parsedStatement, AnalyzeMode.NONE); @@ -523,7 +534,8 @@ public void testChecksumResultSet() { Type.struct( StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), - StructField.of("AMOUNT", Type.numeric())), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json())), Arrays.asList( Struct.newBuilder() .set("ID") @@ -532,6 +544,8 @@ public void testChecksumResultSet() { .to("TEST 1") .set("AMOUNT") .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) .build(), Struct.newBuilder() .set("ID") @@ -540,6 +554,8 @@ public void testChecksumResultSet() { .to("TEST 2") .set("AMOUNT") .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) .build())); ChecksumResultSet rs2 = transaction.createChecksumResultSet(delegate2, parsedStatement, AnalyzeMode.NONE); @@ -549,7 +565,8 @@ public void testChecksumResultSet() { Type.struct( StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), - StructField.of("AMOUNT", Type.numeric())), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json())), Arrays.asList( Struct.newBuilder() .set("ID") @@ -558,6 +575,8 @@ public void testChecksumResultSet() { .to("TEST 2") .set("AMOUNT") .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) .build(), Struct.newBuilder() .set("ID") @@ -566,6 +585,8 @@ public void testChecksumResultSet() { .to("TEST 1") .set("AMOUNT") .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) .build())); ChecksumResultSet rs3 = transaction.createChecksumResultSet(delegate3, parsedStatement, AnalyzeMode.NONE); @@ -576,7 +597,8 @@ public void testChecksumResultSet() { Type.struct( StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), - StructField.of("AMOUNT", Type.numeric())), + StructField.of("AMOUNT", Type.numeric()), + StructField.of("JSON", Type.json())), Arrays.asList( Struct.newBuilder() .set("ID") @@ -585,6 +607,8 @@ public void testChecksumResultSet() { .to("TEST 1") .set("AMOUNT") .to(new BigDecimal("5.50")) + .set("JSON") + .to(Value.json(simpleJson)) .build(), Struct.newBuilder() .set("ID") @@ -593,6 +617,8 @@ public void testChecksumResultSet() { .to("TEST 2") .set("AMOUNT") .to(new BigDecimal("7.50")) + .set("JSON") + .to(Value.json(arrayJson)) .build(), Struct.newBuilder() .set("ID") @@ -601,6 +627,8 @@ public void testChecksumResultSet() { .to("TEST 3") .set("AMOUNT") .to(new BigDecimal("9.99")) + .set("JSON") + .to(Value.json(emptyArrayJson)) .build())); ChecksumResultSet rs4 = transaction.createChecksumResultSet(delegate4, parsedStatement, AnalyzeMode.NONE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java index de10e1c4a5..4d09f0c840 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java @@ -284,6 +284,15 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getStringList("test2"); verify(delegate).getStringList("test2"); + subject.getJson(0); + verify(delegate).getJson(0); + subject.getJson("test0"); + verify(delegate).getJson("test0"); + subject.getJsonList(2); + verify(delegate).getJsonList(2); + subject.getJsonList("test2"); + verify(delegate).getJsonList("test2"); + subject.getStructList(0); subject.getStructList("test0"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index 1900f04213..e0e887624b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -229,6 +229,36 @@ public void bindStringNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindJson() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute( + Statement.newBuilder("SELECT @v") + .bind("v") + .to(Value.json("{\"rating\":9,\"open\":true}")), + Type.json()); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getJson(0)).isEqualTo("{\"open\":true,\"rating\":9}"); + } + + @Test + public void bindJsonEmpty() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute(Statement.newBuilder("SELECT @v").bind("v").to(Value.json("{}")), Type.json()); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getJson(0)).isEqualTo("{}"); + } + + @Test + public void bindJsonNull() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute(Statement.newBuilder("SELECT @v").bind("v").to(Value.json(null)), Type.json()); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindBytes() { Struct row = @@ -431,6 +461,41 @@ public void bindStringArrayNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void bindJsonArray() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute( + Statement.newBuilder("SELECT @v") + .bind("v") + .toJsonArray(asList("{}", "[]", "{\"rating\":9,\"open\":true}", null)), + Type.array(Type.json())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getJsonList(0)) + .containsExactly("{}", "[]", "{\"open\":true,\"rating\":9}", null) + .inOrder(); + } + + @Test + public void bindJsonArrayEmpty() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute( + Statement.newBuilder("SELECT @v").bind("v").toJsonArray(Collections.emptyList()), + Type.array(Type.json())); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getJsonList(0)).isEqualTo(Collections.emptyList()); + } + + @Test + public void bindJsonArrayNull() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + Struct row = + execute( + Statement.newBuilder("SELECT @v").bind("v").toJsonArray(null), Type.array(Type.json())); + assertThat(row.isNull(0)).isTrue(); + } + @Test public void bindBytesArray() { ByteArray e1 = ByteArray.copyFrom("x"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java index fd9fc78ef3..a76c40ea61 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java @@ -17,6 +17,8 @@ package com.google.cloud.spanner.it; import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; +import static com.google.cloud.spanner.Type.array; +import static com.google.cloud.spanner.Type.json; import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -70,14 +72,15 @@ public class ITWriteTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - // TODO: Remove when the emulator supports NUMERIC - private static final String SCHEMA_WITH_NUMERIC = + // TODO: Remove when the emulator supports NUMERIC and JSON + private static final String SCHEMA_WITH_NUMERIC_AND_JSON = "CREATE TABLE T (" + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," + " Int64Value INT64," + " Float64Value FLOAT64," + " StringValue STRING(MAX)," + + " JsonValue JSON," + " BytesValue BYTES(MAX)," + " TimestampValue TIMESTAMP OPTIONS (allow_commit_timestamp = true)," + " DateValue DATE," @@ -86,12 +89,13 @@ public class ITWriteTest { + " Int64ArrayValue ARRAY," + " Float64ArrayValue ARRAY," + " StringArrayValue ARRAY," + + " JsonArrayValue ARRAY," + " BytesArrayValue ARRAY," + " TimestampArrayValue ARRAY," + " DateArrayValue ARRAY," + " NumericArrayValue ARRAY," + ") PRIMARY KEY (K)"; - private static final String SCHEMA_WITHOUT_NUMERIC = + private static final String SCHEMA_WITHOUT_NUMERIC_AND_JSON = "CREATE TABLE T (" + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," @@ -119,10 +123,10 @@ public class ITWriteTest { @BeforeClass public static void setUpDatabase() { if (EmulatorSpannerHelper.isUsingEmulator()) { - // The emulator does not yet support NUMERIC. - db = env.getTestHelper().createTestDatabase(SCHEMA_WITHOUT_NUMERIC); + // The emulator does not yet support NUMERIC or JSON. + db = env.getTestHelper().createTestDatabase(SCHEMA_WITHOUT_NUMERIC_AND_JSON); } else { - db = env.getTestHelper().createTestDatabase(SCHEMA_WITH_NUMERIC); + db = env.getTestHelper().createTestDatabase(SCHEMA_WITH_NUMERIC_AND_JSON); } client = env.getTestHelper().getDatabaseClient(db); } @@ -321,6 +325,35 @@ public void writeStringNull() { assertThat(row.isNull(0)).isTrue(); } + @Test + public void writeJson() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonValue").to(Value.json("{\"rating\":9,\"open\":true}")).build()); + Struct row = readLastRow("JsonValue"); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getColumnType("JsonValue")).isEqualTo(json()); + assertThat(row.getJson(0)).isEqualTo("{\"open\":true,\"rating\":9}"); + } + + @Test + public void writeJsonEmpty() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonValue").to(Value.json("{}")).build()); + Struct row = readLastRow("JsonValue"); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getColumnType("JsonValue")).isEqualTo(json()); + assertThat(row.getJson(0)).isEqualTo("{}"); + } + + @Test + public void writeJsonNull() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonValue").to(Value.json(null)).build()); + Struct row = readLastRow("JsonValue"); + assertThat(row.isNull(0)).isTrue(); + assertThat(row.getColumnType("JsonValue")).isEqualTo(json()); + } + @Test public void writeBytes() { ByteArray data = ByteArray.copyFrom("V1"); @@ -586,6 +619,51 @@ public void writeStringArray() { assertThat(row.getStringList(0)).containsExactly("a", null, "b").inOrder(); } + @Test + public void writeJsonArrayNull() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonArrayValue").toJsonArray(null).build()); + Struct row = readLastRow("JsonArrayValue"); + assertThat(row.isNull(0)).isTrue(); + assertThat(row.getColumnType("JsonArrayValue")).isEqualTo(array(json())); + } + + @Test + public void writeJsonArrayEmpty() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonArrayValue").toJsonArray(Collections.emptyList()).build()); + Struct row = readLastRow("JsonArrayValue"); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getColumnType("JsonArrayValue")).isEqualTo(array(json())); + assertThat(row.getJsonList(0)).containsExactly(); + } + + @Test + public void writeJsonArray() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write(baseInsert().set("JsonArrayValue").toJsonArray(Arrays.asList("[]", null, "{}")).build()); + Struct row = readLastRow("JsonArrayValue"); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getColumnType("JsonArrayValue")).isEqualTo(array(json())); + assertThat(row.getJsonList(0)).containsExactly("[]", null, "{}").inOrder(); + } + + @Test + public void writeJsonArrayNoNulls() { + assumeFalse("Emulator does not yet support JSON", EmulatorSpannerHelper.isUsingEmulator()); + write( + baseInsert() + .set("JsonArrayValue") + .toJsonArray(Arrays.asList("[]", "{\"color\":\"red\",\"value\":\"#f00\"}", "{}")) + .build()); + Struct row = readLastRow("JsonArrayValue"); + assertThat(row.isNull(0)).isFalse(); + assertThat(row.getColumnType("JsonArrayValue")).isEqualTo(array(json())); + assertThat(row.getJsonList(0)) + .containsExactly("[]", "{\"color\":\"red\",\"value\":\"#f00\"}", "{}") + .inOrder(); + } + @Test public void writeBytesArrayNull() { write(baseInsert().set("BytesArrayValue").toBytesArray(null).build()); diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json index 8f30964036..1d6d3b21b1 100644 --- a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/read_tests.json @@ -3,6 +3,7 @@ "result": {"value": [[ true, "abc", + "{\"color\":\"red\",\"value\":\"#f00\"}", "100", 1.1, "3.141592", @@ -19,7 +20,7 @@ ["ghi"] ] ]]}, - "chunks": ["{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"BOOL\"\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": \"INT64\"\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": \"FLOAT64\"\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": \"NUMERIC\"\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": \"BYTES\"\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRING\"\n }\n }\n }, {\n \"name\": \"f8\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRUCT\",\n \"structType\": {\n \"fields\": [{\n \"name\": \"f81\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [true, \"abc\", \"100\", 1.1, \"3.141592\", \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}"], + "chunks": ["{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"BOOL\"\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": \"JSON\"\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": \"INT64\"\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": \"FLOAT64\"\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": \"NUMERIC\"\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": \"BYTES\"\n }\n }, {\n \"name\": \"f8\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRING\"\n }\n }\n }, {\n \"name\": \"f9\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"STRUCT\",\n \"structType\": {\n \"fields\": [{\n \"name\": \"f81\",\n \"type\": {\n \"code\": \"STRING\"\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [true, \"abc\", \"{\\\"color\\\":\\\"red\\\",\\\"value\\\":\\\"#f00\\\"}\", \"100\", 1.1, \"3.141592\", \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}"], "name": "Basic Test" }, { @@ -84,6 +85,14 @@ ], "name": "String Array Chunking Test With One Large String" }, + { + "result": {"value": [[["{}", "{\"color\":\"red\",\"value\":\"#f00\"}"]]]}, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": \"ARRAY\",\n \"arrayElementType\": {\n \"code\": \"JSON\"\n }\n }\n }]\n }\n },\n \"values\": [[\"{}\", \"{\\\"color\\\":\\\"red\\\"\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\",\\\"value\\\":\\\"#f00\\\"}\"]],\n \"chunkedValue\": false\n}" + ], + "name": "JSON Array Chunking Test" + }, { "result": {"value": [[[ "1",