diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index fb952176bd..8d597d29b3 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -131,4 +131,25 @@
com/google/cloud/spanner/spi/v1/GapicSpannerRpc
com.google.iam.v1.Policy getDatabaseAdminIAMPolicy(java.lang.String)
+
+
+ 7012
+ com/google/cloud/spanner/StructReader
+ java.lang.String getPgJsonb(int)
+
+
+ 7012
+ com/google/cloud/spanner/StructReader
+ java.lang.String getPgJsonb(java.lang.String)
+
+
+ 7012
+ com/google/cloud/spanner/StructReader
+ java.util.List getPgJsonbList(int)
+
+
+ 7012
+ com/google/cloud/spanner/StructReader
+ java.util.List getPgJsonbList(java.lang.String)
+
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 bc4f224b93..6ccb28900f 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
@@ -67,6 +67,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** Implementation of {@link ResultSet}. */
@@ -384,6 +385,9 @@ private Object writeReplace() {
case JSON:
builder.set(fieldName).to(Value.json((String) value));
break;
+ case PG_JSONB:
+ builder.set(fieldName).to(Value.pgJsonb((String) value));
+ break;
case BYTES:
builder.set(fieldName).to((ByteArray) value);
break;
@@ -417,6 +421,9 @@ private Object writeReplace() {
case JSON:
builder.set(fieldName).toJsonArray((Iterable) value);
break;
+ case PG_JSONB:
+ builder.set(fieldName).toPgJsonbArray((Iterable) value);
+ break;
case BYTES:
builder.set(fieldName).toBytesArray((Iterable) value);
break;
@@ -491,10 +498,9 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot
checkType(fieldType, proto, KindCase.STRING_VALUE);
return new BigDecimal(proto.getStringValue());
case PG_NUMERIC:
- checkType(fieldType, proto, KindCase.STRING_VALUE);
- return proto.getStringValue();
case STRING:
case JSON:
+ case PG_JSONB:
checkType(fieldType, proto, KindCase.STRING_VALUE);
return proto.getStringValue();
case BYTES:
@@ -558,14 +564,14 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) {
return list;
}
case PG_NUMERIC:
- return Lists.transform(
- listValue.getValuesList(),
- input -> input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue());
case STRING:
case JSON:
- return Lists.transform(
- listValue.getValuesList(),
- input -> input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue());
+ case PG_JSONB:
+ return listValue.getValuesList().stream()
+ .map(
+ input ->
+ input.getKindCase() == KindCase.NULL_VALUE ? null : input.getStringValue())
+ .collect(Collectors.toList());
case BYTES:
{
// Materialize list: element conversion is expensive and should happen only once.
@@ -679,6 +685,11 @@ protected String getJsonInternal(int columnIndex) {
return (String) rowData.get(columnIndex);
}
+ @Override
+ protected String getPgJsonbInternal(int columnIndex) {
+ return (String) rowData.get(columnIndex);
+ }
+
@Override
protected ByteArray getBytesInternal(int columnIndex) {
return (ByteArray) rowData.get(columnIndex);
@@ -715,6 +726,8 @@ protected Value getValueInternal(int columnIndex) {
return Value.string(isNull ? null : getStringInternal(columnIndex));
case JSON:
return Value.json(isNull ? null : getJsonInternal(columnIndex));
+ case PG_JSONB:
+ return Value.pgJsonb(isNull ? null : getPgJsonbInternal(columnIndex));
case BYTES:
return Value.bytes(isNull ? null : getBytesInternal(columnIndex));
case TIMESTAMP:
@@ -740,6 +753,8 @@ protected Value getValueInternal(int columnIndex) {
return Value.stringArray(isNull ? null : getStringListInternal(columnIndex));
case JSON:
return Value.jsonArray(isNull ? null : getJsonListInternal(columnIndex));
+ case PG_JSONB:
+ return Value.pgJsonbArray(isNull ? null : getPgJsonbListInternal(columnIndex));
case BYTES:
return Value.bytesArray(isNull ? null : getBytesListInternal(columnIndex));
case TIMESTAMP:
@@ -816,11 +831,17 @@ protected List getStringListInternal(int columnIndex) {
}
@Override
- @SuppressWarnings("unchecked") // We know ARRAY produces a List.
+ @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 getPgJsonbListInternal(int columnIndex) {
+ return Collections.unmodifiableList((List) rowData.get(columnIndex));
+ }
+
@Override
@SuppressWarnings("unchecked") // We know ARRAY produces a List.
protected List getBytesListInternal(int columnIndex) {
@@ -1352,6 +1373,11 @@ protected String getJsonInternal(int columnIndex) {
return currRow().getJsonInternal(columnIndex);
}
+ @Override
+ protected String getPgJsonbInternal(int columnIndex) {
+ return currRow().getPgJsonbInternal(columnIndex);
+ }
+
@Override
protected ByteArray getBytesInternal(int columnIndex) {
return currRow().getBytesInternal(columnIndex);
@@ -1417,6 +1443,11 @@ protected List getJsonListInternal(int columnIndex) {
return currRow().getJsonListInternal(columnIndex);
}
+ @Override
+ protected List getPgJsonbListInternal(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 d9038466a4..1e89763624 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
@@ -48,6 +48,10 @@ protected String getJsonInternal(int columnIndex) {
throw new UnsupportedOperationException("Not implemented");
}
+ protected String getPgJsonbInternal(int columnIndex) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
protected abstract ByteArray getBytesInternal(int columnIndex);
protected abstract Timestamp getTimestampInternal(int columnIndex);
@@ -78,6 +82,10 @@ protected List getJsonListInternal(int columnIndex) {
throw new UnsupportedOperationException("Not implemented");
}
+ protected List getPgJsonbListInternal(int columnIndex) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
protected abstract List getBytesListInternal(int columnIndex);
protected abstract List getTimestampListInternal(int columnIndex);
@@ -189,6 +197,19 @@ public String getJson(String columnName) {
return getJsonInternal(columnIndex);
}
+ @Override
+ public String getPgJsonb(int columnIndex) {
+ checkNonNullOfType(columnIndex, Type.pgJsonb(), columnIndex);
+ return getPgJsonbInternal(columnIndex);
+ }
+
+ @Override
+ public String getPgJsonb(String columnName) {
+ int columnIndex = getColumnIndex(columnName);
+ checkNonNullOfType(columnIndex, Type.pgJsonb(), columnName);
+ return getPgJsonbInternal(columnIndex);
+ }
+
@Override
public ByteArray getBytes(int columnIndex) {
checkNonNullOfType(columnIndex, Type.bytes(), columnIndex);
@@ -365,6 +386,19 @@ public List getJsonList(String columnName) {
return getJsonListInternal(columnIndex);
}
+ @Override
+ public List getPgJsonbList(int columnIndex) {
+ checkNonNullOfType(columnIndex, Type.array(Type.pgJsonb()), columnIndex);
+ return getPgJsonbListInternal(columnIndex);
+ }
+
+ @Override
+ public List getPgJsonbList(String columnName) {
+ int columnIndex = getColumnIndex(columnName);
+ checkNonNullOfType(columnIndex, Type.array(Type.pgJsonb()), columnName);
+ return getPgJsonbListInternal(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 e225bdcc1b..2a85006fa9 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
@@ -168,6 +168,18 @@ public String getJson(String columnName) {
return delegate.get().getJson(columnName);
}
+ @Override
+ public String getPgJsonb(int columnIndex) {
+ checkValidState();
+ return delegate.get().getPgJsonb(columnIndex);
+ }
+
+ @Override
+ public String getPgJsonb(String columnName) {
+ checkValidState();
+ return delegate.get().getPgJsonb(columnName);
+ }
+
@Override
public ByteArray getBytes(int columnIndex) {
checkValidState();
@@ -310,6 +322,18 @@ public List getJsonList(String columnName) {
return delegate.get().getJsonList(columnName);
}
+ @Override
+ public List getPgJsonbList(int columnIndex) {
+ checkValidState();
+ return delegate.get().getPgJsonbList(columnIndex);
+ }
+
+ @Override
+ public List getPgJsonbList(String columnName) {
+ checkValidState();
+ return delegate.get().getPgJsonbList(columnName);
+ }
+
@Override
public List getBytesList(int columnIndex) {
checkValidState();
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 af57b5b848..6eacd3208e 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
@@ -253,6 +253,16 @@ public String getJson(String columnName) {
return getCurrentRowAsStruct().getJson(columnName);
}
+ @Override
+ public String getPgJsonb(int columnIndex) {
+ return getCurrentRowAsStruct().getPgJsonb(columnIndex);
+ }
+
+ @Override
+ public String getPgJsonb(String columnName) {
+ return getCurrentRowAsStruct().getPgJsonb(columnName);
+ }
+
@Override
public ByteArray getBytes(int columnIndex) {
return getCurrentRowAsStruct().getBytes(columnIndex);
@@ -383,6 +393,16 @@ public List getJsonList(String columnName) {
return getCurrentRowAsStruct().getJsonList(columnName);
}
+ @Override
+ public List getPgJsonbList(int columnIndex) {
+ return getCurrentRowAsStruct().getPgJsonbList(columnIndex);
+ }
+
+ @Override
+ public List getPgJsonbList(String columnName) {
+ return getCurrentRowAsStruct().getPgJsonbList(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 c986767d3a..48c989d145 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
@@ -197,6 +197,11 @@ protected String getJsonInternal(int columnIndex) {
return values.get(columnIndex).getJson();
}
+ @Override
+ protected String getPgJsonbInternal(int columnIndex) {
+ return values.get(columnIndex).getPgJsonb();
+ }
+
@Override
protected ByteArray getBytesInternal(int columnIndex) {
return values.get(columnIndex).getBytes();
@@ -267,6 +272,11 @@ protected List getJsonListInternal(int columnIndex) {
return values.get(columnIndex).getJsonArray();
}
+ @Override
+ protected List getPgJsonbListInternal(int columnIndex) {
+ return values.get(columnIndex).getPgJsonbArray();
+ }
+
@Override
protected List getBytesListInternal(int columnIndex) {
return values.get(columnIndex).getBytesArray();
@@ -355,6 +365,8 @@ private Object getAsObject(int columnIndex) {
return getStringInternal(columnIndex);
case JSON:
return getJsonInternal(columnIndex);
+ case PG_JSONB:
+ return getPgJsonbInternal(columnIndex);
case BYTES:
return getBytesInternal(columnIndex);
case TIMESTAMP:
@@ -379,6 +391,8 @@ private Object getAsObject(int columnIndex) {
return getStringListInternal(columnIndex);
case JSON:
return getJsonListInternal(columnIndex);
+ case PG_JSONB:
+ return getPgJsonbListInternal(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 3779e8067d..a96c95cb95 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,16 +114,26 @@ 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()}. */
+ /** Returns the value of a non-{@code NULL} column with type {@link Type#json()}. */
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()}. */
+ /** Returns the value of a non-{@code NULL} column with type {@link Type#json()}. */
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#pgJsonb()}. */
+ default String getPgJsonb(int columnIndex) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /** Returns the value of a non-{@code NULL} column with type {@link Type#pgJsonb()}. */
+ default String getPgJsonb(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);
@@ -238,16 +248,30 @@ 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())}. */
+ /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.json())}. */
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())}. */
+ /** Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.json())}. */
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.pgJsonb())}.
+ */
+ default List getPgJsonbList(int columnIndex) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ };
+
+ /**
+ * Returns the value of a non-{@code NULL} column with type {@code Type.array(Type.pgJsonb())}.
+ */
+ default List getPgJsonbList(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 15305e0cda..7ba6b9a41e 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
@@ -52,6 +52,7 @@ public final class Type implements Serializable {
private static final Type TYPE_PG_NUMERIC = new Type(Code.PG_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_PG_JSONB = new Type(Code.PG_JSONB, 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);
@@ -62,6 +63,7 @@ public final class Type implements Serializable {
private static final Type TYPE_ARRAY_PG_NUMERIC = new Type(Code.ARRAY, TYPE_PG_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_PG_JSONB = new Type(Code.ARRAY, TYPE_PG_JSONB, 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);
@@ -115,6 +117,11 @@ public static Type json() {
return TYPE_JSON;
}
+ /** Returns the descriptor for the {@code JSONB} type. */
+ public static Type pgJsonb() {
+ return TYPE_PG_JSONB;
+ }
+
/** Returns the descriptor for the {@code BYTES} type: a variable-length byte string. */
public static Type bytes() {
return TYPE_BYTES;
@@ -154,6 +161,8 @@ public static Type array(Type elementType) {
return TYPE_ARRAY_STRING;
case JSON:
return TYPE_ARRAY_JSON;
+ case PG_JSONB:
+ return TYPE_ARRAY_PG_JSONB;
case BYTES:
return TYPE_ARRAY_BYTES;
case TIMESTAMP:
@@ -209,6 +218,7 @@ public enum Code {
FLOAT64(TypeCode.FLOAT64),
STRING(TypeCode.STRING),
JSON(TypeCode.JSON),
+ PG_JSONB(TypeCode.JSON, TypeAnnotationCode.PG_JSONB),
BYTES(TypeCode.BYTES),
TIMESTAMP(TypeCode.TIMESTAMP),
DATE(TypeCode.DATE),
@@ -446,6 +456,8 @@ static Type fromProto(com.google.spanner.v1.Type proto) {
return string();
case JSON:
return json();
+ case PG_JSONB:
+ return pgJsonb();
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 e3c53de937..3ec7b67f65 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
@@ -200,7 +200,7 @@ public static Value string(@Nullable String v) {
}
/**
- * Returns a {@code STRING} value.
+ * Returns a {@code JSON} value.
*
* @param v the value, which may be null
*/
@@ -208,6 +208,15 @@ public static Value json(@Nullable String v) {
return new JsonImpl(v == null, v);
}
+ /**
+ * Returns a {@code PG JSONB} value.
+ *
+ * @param v the value, which may be null
+ */
+ public static Value pgJsonb(@Nullable String v) {
+ return new PgJsonbImpl(v == null, v);
+ }
+
/**
* Returns a {@code BYTES} value.
*
@@ -393,7 +402,7 @@ public static Value stringArray(@Nullable Iterable v) {
}
/**
- * Returns an {@code ARRAY} value.
+ * 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}.
@@ -402,6 +411,16 @@ public static Value jsonArray(@Nullable Iterable v) {
return new JsonArrayImpl(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 pgJsonbArray(@Nullable Iterable v) {
+ return new PgJsonbArrayImpl(v == null, v == null ? null : immutableCopyOf(v));
+ }
+
/**
* Returns an {@code ARRAY} value.
*
@@ -513,6 +532,15 @@ public String getJson() {
throw new UnsupportedOperationException("Not implemented");
}
+ /**
+ * Returns the value of a {@code JSONB}-typed instance.
+ *
+ * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+ */
+ public String getPgJsonb() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
/**
* Returns the value of a {@code BYTES}-typed instance.
*
@@ -595,6 +623,16 @@ 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.
+ *
+ * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+ */
+ public List getPgJsonbArray() {
+ 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.
@@ -808,6 +846,11 @@ public String getJson() {
throw defaultGetter(Type.json());
}
+ @Override
+ public String getPgJsonb() {
+ throw defaultGetter(Type.pgJsonb());
+ }
+
@Override
public ByteArray getBytes() {
throw defaultGetter(Type.bytes());
@@ -862,6 +905,11 @@ public List getJsonArray() {
throw defaultGetter(Type.array(Type.json()));
}
+ @Override
+ public List getPgJsonbArray() {
+ throw defaultGetter(Type.array(Type.pgJsonb()));
+ }
+
@Override
public List getBytesArray() {
throw defaultGetter(Type.array(Type.bytes()));
@@ -1229,6 +1277,34 @@ void valueToString(StringBuilder b) {
}
}
+ private static class PgJsonbImpl extends AbstractObjectValue {
+
+ private PgJsonbImpl(boolean isNull, @Nullable String value) {
+ super(isNull, Type.pgJsonb(), value);
+ }
+
+ @Override
+ public String getPgJsonb() {
+ checkType(Type.pgJsonb());
+ checkNotNull();
+ return value;
+ }
+
+ @Override
+ public String getString() {
+ return getPgJsonb();
+ }
+
+ @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) {
@@ -1666,6 +1742,30 @@ void appendElement(StringBuilder b, String element) {
}
}
+ private static class PgJsonbArrayImpl extends AbstractArrayValue {
+
+ private PgJsonbArrayImpl(boolean isNull, @Nullable List values) {
+ super(isNull, Type.pgJsonb(), values);
+ }
+
+ @Override
+ public List getPgJsonbArray() {
+ checkType(getType());
+ checkNotNull();
+ return value;
+ }
+
+ @Override
+ public List getStringArray() {
+ return this.getPgJsonbArray();
+ }
+
+ @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);
@@ -1857,6 +1957,8 @@ private Value getValue(int fieldIndex) {
return Value.string(value.getString(fieldIndex));
case JSON:
return Value.json(value.getJson(fieldIndex));
+ case PG_JSONB:
+ return Value.pgJsonb(value.getPgJsonb(fieldIndex));
case BYTES:
return Value.bytes(value.getBytes(fieldIndex));
case FLOAT64:
@@ -1883,6 +1985,8 @@ private Value getValue(int fieldIndex) {
return Value.stringArray(value.getStringList(fieldIndex));
case JSON:
return Value.jsonArray(value.getJsonList(fieldIndex));
+ case PG_JSONB:
+ return Value.pgJsonbArray(value.getPgJsonbList(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 cdca5d84a2..ec9e5a43d8 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
@@ -188,6 +188,11 @@ public R toJsonArray(@Nullable Iterable values) {
return handle(Value.jsonArray(values));
}
+ /** Binds to {@code Value.jsonbArray(values)} */
+ public R toPgJsonbArray(@Nullable Iterable values) {
+ return handle(Value.pgJsonbArray(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 2c01396083..bb2f2fb817 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
@@ -248,6 +248,9 @@ public void funnel(Struct row, PrimitiveSink into) {
case JSON:
funnelValue(type, row.getJson(i), into);
break;
+ case PG_JSONB:
+ funnelValue(type, row.getPgJsonb(i), into);
+ break;
case TIMESTAMP:
funnelValue(type, row.getTimestamp(i), into);
break;
@@ -318,6 +321,12 @@ private void funnelArray(
funnelValue(Code.JSON, value, into);
}
break;
+ case PG_JSONB:
+ into.putInt(row.getPgJsonbList(columnIndex).size());
+ for (String value : row.getPgJsonbList(columnIndex)) {
+ funnelValue(Code.PG_JSONB, value, into);
+ }
+ break;
case TIMESTAMP:
into.putInt(row.getTimestampList(columnIndex).size());
for (Timestamp value : row.getTimestampList(columnIndex)) {
@@ -370,6 +379,7 @@ private void funnelValue(Code type, T value, PrimitiveSink into) {
case PG_NUMERIC:
case STRING:
case JSON:
+ case PG_JSONB:
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 21c3dae7ec..8fb0bbe440 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
@@ -203,6 +203,18 @@ public String getJson(String columnName) {
return delegate.getJson(columnName);
}
+ @Override
+ public String getPgJsonb(int columnIndex) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getPgJsonb(columnIndex);
+ }
+
+ @Override
+ public String getPgJsonb(String columnName) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getPgJsonb(columnName);
+ }
+
@Override
public ByteArray getBytes(int columnIndex) {
Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
@@ -359,6 +371,18 @@ public List getJsonList(String columnName) {
return delegate.getJsonList(columnName);
}
+ @Override
+ public List getPgJsonbList(int columnIndex) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getPgJsonbList(columnIndex);
+ }
+
+ @Override
+ public List getPgJsonbList(String columnName) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getPgJsonbList(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 6bf8f046c5..cc9759a487 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
@@ -208,6 +208,18 @@ public String getJson(String columnName) {
return delegate.getJson(columnName);
}
+ @Override
+ public String getPgJsonb(int columnIndex) {
+ checkClosed();
+ return delegate.getPgJsonb(columnIndex);
+ }
+
+ @Override
+ public String getPgJsonb(String columnName) {
+ checkClosed();
+ return delegate.getPgJsonb(columnName);
+ }
+
@Override
public ByteArray getBytes(int columnIndex) {
checkClosed();
@@ -364,6 +376,18 @@ public List getJsonList(String columnName) {
return delegate.getJsonList(columnName);
}
+ @Override
+ public List getPgJsonbList(int columnIndex) {
+ checkClosed();
+ return delegate.getPgJsonbList(columnIndex);
+ }
+
+ @Override
+ public List getPgJsonbList(String columnName) {
+ checkClosed();
+ return delegate.getPgJsonbList(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 10167ddc9d..1b6280a636 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
@@ -75,6 +75,11 @@ protected String getJsonInternal(int columnIndex) {
return null;
}
+ @Override
+ protected String getPgJsonbInternal(int columnIndex) {
+ return null;
+ }
+
@Override
protected ByteArray getBytesInternal(int columnIndex) {
return null;
@@ -140,6 +145,11 @@ protected List getJsonListInternal(int columnIndex) {
return null;
}
+ @Override
+ protected List getPgJsonbListInternal(int columnIndex) {
+ return null;
+ }
+
@Override
protected List getBytesListInternal(int columnIndex) {
return null;
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 a11ac78b54..ff4e92a521 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
@@ -716,6 +716,25 @@ public void getJson() {
assertEquals("[]", resultSet.getJson(0));
}
+ @Test
+ public void getPgJsonb() {
+ consumer.onPartialResultSet(
+ PartialResultSet.newBuilder()
+ .setMetadata(makeMetadata(Type.struct(Type.StructField.of("f", Type.pgJsonb()))))
+ .addValues(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#f00\"}").toProto())
+ .addValues(Value.pgJsonb("{}").toProto())
+ .addValues(Value.pgJsonb("[]").toProto())
+ .build());
+ consumer.onCompleted();
+
+ assertTrue(resultSet.next());
+ assertEquals("{\"color\":\"red\",\"value\":\"#f00\"}", resultSet.getPgJsonb(0));
+ assertTrue(resultSet.next());
+ assertEquals("{}", resultSet.getPgJsonb(0));
+ assertTrue(resultSet.next());
+ assertEquals("[]", resultSet.getPgJsonb(0));
+ }
+
@Test
public void getBooleanArray() {
boolean[] boolArray = {true, true, false};
@@ -838,4 +857,23 @@ public void getJsonList() {
assertTrue(resultSet.next());
assertEquals(jsonList, resultSet.getJsonList(0));
}
+
+ @Test
+ public void getPgJsonbList() {
+ 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.pgJsonb())))))
+ .addValues(Value.pgJsonbArray(jsonList).toProto())
+ .build());
+ consumer.onCompleted();
+
+ assertTrue(resultSet.next());
+ assertEquals(jsonList, resultSet.getPgJsonbList(0));
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
index eb96334b50..fe2b7aec94 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java
@@ -541,6 +541,14 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) {
.to(Value.numeric(BigDecimal.TEN))
.set("pgNumericValue")
.to(Value.pgNumeric("4.2"))
+ .set("json")
+ .to(Value.json("{\"key\": \"value\"}}"))
+ .set("jsonNull")
+ .to(Value.json(null))
+ .set("pgJsonb")
+ .to(Value.pgJsonb("{\"key\": \"value\"}}"))
+ .set("pgJsonbNull")
+ .to(Value.pgJsonb(null))
.set("timestamp")
.to(Timestamp.MAX_VALUE)
.set("timestampNull")
@@ -589,6 +597,18 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) {
.toPgNumericArray(null)
.set("pgNumericArrValue")
.to(Value.pgNumericArray(ImmutableList.of("10.20", "20.30")))
+ .set("jsonArr")
+ .toJsonArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}"))
+ .set("jsonArrNull")
+ .toJsonArray(null)
+ .set("jsonArrValue")
+ .to(Value.jsonArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}")))
+ .set("pgJsonbArr")
+ .toPgJsonbArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}"))
+ .set("pgJsonbArrNull")
+ .toPgJsonbArray(null)
+ .set("pgJsonbArrValue")
+ .to(Value.pgJsonbArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}")))
.set("timestampArr")
.toTimestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE))
.set("timestampArrNull")
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 85cdc0f687..87be602808 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
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
@@ -89,6 +90,7 @@ public void resultSetIteration() {
Type.StructField.of("bigDecimalVal", Type.numeric()),
Type.StructField.of("stringVal", Type.string()),
Type.StructField.of("jsonVal", Type.json()),
+ Type.StructField.of("pgJsonbVal", Type.pgJsonb()),
Type.StructField.of("byteVal", Type.bytes()),
Type.StructField.of("timestamp", Type.timestamp()),
Type.StructField.of("date", Type.date()),
@@ -100,7 +102,8 @@ public void resultSetIteration() {
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("jsonArray", Type.array(Type.json())));
+ Type.StructField.of("jsonArray", Type.array(Type.json())),
+ Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())));
Struct struct1 =
Struct.newBuilder()
.set("f1")
@@ -117,6 +120,8 @@ public void resultSetIteration() {
.to(stringVal)
.set("jsonVal")
.to(Value.json(jsonVal))
+ .set("pgJsonbVal")
+ .to(Value.pgJsonb(jsonVal))
.set("byteVal")
.to(Value.bytes(ByteArray.copyFrom(byteVal)))
.set("timestamp")
@@ -141,6 +146,8 @@ public void resultSetIteration() {
.to(Value.stringArray(Arrays.asList(stringArray)))
.set("jsonArray")
.to(Value.jsonArray(Arrays.asList(jsonArray)))
+ .set("pgJsonbArray")
+ .to(Value.pgJsonbArray(Arrays.asList(jsonArray)))
.build();
Struct struct2 =
Struct.newBuilder()
@@ -158,6 +165,8 @@ public void resultSetIteration() {
.to(stringVal)
.set("jsonVal")
.to(Value.json(jsonVal))
+ .set("pgJsonbVal")
+ .to(Value.pgJsonb(jsonVal))
.set("byteVal")
.to(Value.bytes(ByteArray.copyFrom(byteVal)))
.set("timestamp")
@@ -182,10 +191,12 @@ public void resultSetIteration() {
.to(Value.stringArray(Arrays.asList(stringArray)))
.set("jsonArray")
.to(Value.jsonArray(Arrays.asList(jsonArray)))
+ .set("pgJsonbArray")
+ .to(Value.pgJsonbArray(Arrays.asList(jsonArray)))
.build();
ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2));
- IllegalStateException e = assertThrows(IllegalStateException.class, () -> rs.getType());
+ IllegalStateException e = assertThrows(IllegalStateException.class, rs::getType);
assertThat(e.getMessage()).contains("Must be preceded by a next() call");
int columnIndex = 0;
@@ -227,6 +238,12 @@ public void resultSetIteration() {
assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.json(jsonVal));
assertThat(rs.getJson("jsonVal")).isEqualTo(jsonVal);
assertThat(rs.getValue("jsonVal")).isEqualTo(Value.json(jsonVal));
+
+ assertEquals(jsonVal, rs.getPgJsonb(columnIndex));
+ assertEquals(Value.pgJsonb(jsonVal), rs.getValue(columnIndex++));
+ assertEquals(jsonVal, rs.getPgJsonb("pgJsonbVal"));
+ assertEquals(Value.pgJsonb(jsonVal), rs.getValue("pgJsonbVal"));
+
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));
@@ -285,9 +302,12 @@ public void resultSetIteration() {
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(columnIndex++)).isEqualTo(Arrays.asList(jsonArray));
assertThat(rs.getJsonList("jsonArray")).isEqualTo(Arrays.asList(jsonArray));
+ assertEquals(Arrays.asList(jsonArray), rs.getPgJsonbList(columnIndex));
+ assertEquals(Arrays.asList(jsonArray), rs.getPgJsonbList("pgJsonbArray"));
+
assertThat(rs.next()).isTrue();
assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct2);
assertThat(rs.getString(0)).isEqualTo("y");
@@ -296,7 +316,7 @@ public void resultSetIteration() {
assertThat(rs.next()).isFalse();
UnsupportedOperationException unsupported =
- assertThrows(UnsupportedOperationException.class, () -> rs.getStats());
+ assertThrows(UnsupportedOperationException.class, rs::getStats);
assertThat(unsupported.getMessage())
.contains("ResultSetStats are available only for results returned from analyzeQuery");
}
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 7dfe9f3a98..3ed6fc6c57 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
@@ -152,6 +152,16 @@ Type newType() {
}.test();
}
+ @Test
+ public void pgJsonb() {
+ new ScalarTypeTester(Code.PG_JSONB, TypeCode.JSON, TypeAnnotationCode.PG_JSONB) {
+ @Override
+ Type newType() {
+ return Type.pgJsonb();
+ }
+ }.test();
+ }
+
@Test
public void bytes() {
new ScalarTypeTester(Type.Code.BYTES, TypeCode.BYTES) {
@@ -306,6 +316,16 @@ Type newElementType() {
}.test();
}
+ @Test
+ public void pgJsonbArray() {
+ new ArrayTypeTester(Code.PG_JSONB, TypeCode.JSON, TypeAnnotationCode.PG_JSONB, true) {
+ @Override
+ Type newElementType() {
+ return Type.pgJsonb();
+ }
+ }.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 ea26f09c2e..91263457ba 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
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultJson;
+import static com.google.cloud.spanner.ValueBinderTest.DefaultValues.defaultPgJsonb;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -37,6 +38,7 @@
@RunWith(JUnit4.class)
public class ValueBinderTest {
private static final String JSON_METHOD_NAME = "json";
+ private static final String PG_JSONB_METHOD_NAME = "pgJsonb";
private static final String PG_NUMERIC_METHOD_NAME = "pgNumeric";
public static final String DEFAULT_PG_NUMERIC = "1.23";
@@ -125,6 +127,9 @@ public void reflection()
// ValueBinder.to(Value)
binderMethod = ValueBinder.class.getMethod("to", Value.class);
assertThat(binderMethod.invoke(binder, Value.json(null))).isEqualTo(lastReturnValue);
+ } else if (method.getName().equalsIgnoreCase(PG_JSONB_METHOD_NAME)) {
+ binderMethod = ValueBinder.class.getMethod("to", Value.class);
+ assertThat(binderMethod.invoke(binder, Value.pgJsonb(null))).isEqualTo(lastReturnValue);
} else if (method.getName().equalsIgnoreCase(PG_NUMERIC_METHOD_NAME)) {
binderMethod = ValueBinder.class.getMethod("to", Value.class);
assertThat(binderMethod.invoke(binder, Value.pgNumeric(null)))
@@ -145,6 +150,11 @@ public void reflection()
binderMethod = ValueBinder.class.getMethod("to", Value.class);
assertThat(binderMethod.invoke(binder, Value.json(defaultJson())))
.isEqualTo(lastReturnValue);
+ } else if (method.getName().equalsIgnoreCase(PG_JSONB_METHOD_NAME)) {
+ defaultObject = defaultPgJsonb();
+ binderMethod = ValueBinder.class.getMethod("to", Value.class);
+ assertThat(binderMethod.invoke(binder, Value.pgJsonb(defaultPgJsonb())))
+ .isEqualTo(lastReturnValue);
} else if (method.getName().equalsIgnoreCase(PG_NUMERIC_METHOD_NAME)) {
defaultObject = DEFAULT_PG_NUMERIC;
binderMethod = ValueBinder.class.getMethod("to", Value.class);
@@ -232,6 +242,10 @@ public static String defaultJson() {
return "{\"color\":\"red\",\"value\":\"#f00\"}";
}
+ public static String defaultPgJsonb() {
+ return "{\"color\":\"red\",\"value\":\"#f00\"}";
+ }
+
public static ByteArray defaultByteArray() {
return ByteArray.copyFrom(new byte[] {'x'});
}
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 54f6799e8c..a466fab1ab 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
@@ -491,6 +491,56 @@ public void jsonNested() {
assertEquals(json, v.getJson());
}
+ @Test
+ public void testPgJsonb() {
+ String json = "{\"color\":\"red\",\"value\":\"#f00\"}";
+ Value v = Value.pgJsonb(json);
+ assertEquals(Type.pgJsonb(), v.getType());
+ assertFalse(v.isNull());
+ assertEquals(json, v.getPgJsonb());
+ assertEquals(json, v.getString());
+ }
+
+ @Test
+ public void testPgJsonbNull() {
+ Value v = Value.pgJsonb(null);
+ assertEquals(Type.pgJsonb(), v.getType());
+ assertTrue(v.isNull());
+ assertEquals(NULL_STRING, v.toString());
+ assertThrowsWithMessage(v::getPgJsonb, "null value");
+ assertThrowsWithMessage(v::getString, "null value");
+ }
+
+ @Test
+ public void testPgJsonbEmpty() {
+ String json = "{}";
+ Value v = Value.pgJsonb(json);
+ assertEquals(json, v.getPgJsonb());
+ }
+
+ @Test
+ public void testPgJsonbWithEmptyArray() {
+ String json = "[]";
+ Value v = Value.pgJsonb(json);
+ assertEquals(json, v.getPgJsonb());
+ }
+
+ @Test
+ public void testPgJsonbWithArray() {
+ 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.pgJsonb(json);
+ assertEquals(json, v.getPgJsonb());
+ }
+
+ @Test
+ public void testPgJsonbNested() {
+ 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.pgJsonb(json);
+ assertEquals(json, v.getPgJsonb());
+ }
+
@Test
public void bytes() {
ByteArray bytes = newByteArray("abc");
@@ -894,6 +944,41 @@ public void jsonArrayTryGetFloat64Array() {
assertThrowsWithMessage(value::getFloat64Array, "Expected: ARRAY actual: ARRAY");
}
+ @Test
+ public void testPgJsonbArray() {
+ String one = "{}";
+ String two = null;
+ String three = "{\"color\":\"red\",\"value\":\"#f00\"}";
+ Value v = Value.pgJsonbArray(Arrays.asList(one, two, three));
+ assertFalse(v.isNull());
+ assertArrayEquals(new String[] {one, two, three}, v.getPgJsonbArray().toArray());
+ assertEquals("[{},NULL,{\"color\":\"red\",\"value\":\"#f00\"}]", v.toString());
+ assertArrayEquals(new String[] {one, two, three}, v.getStringArray().toArray());
+ }
+
+ @Test
+ public void testPgJsonbArrayNull() {
+ Value v = Value.pgJsonbArray(null);
+ assertTrue(v.isNull());
+ assertEquals(NULL_STRING, v.toString());
+ assertThrowsWithMessage(v::getPgJsonbArray, "null value");
+ assertThrowsWithMessage(v::getStringArray, "null value");
+ }
+
+ @Test
+ public void testPgJsonbArrayTryGetBytesArray() {
+ Value value = Value.pgJsonbArray(Collections.singletonList("{}"));
+ assertThrowsWithMessage(
+ value::getBytesArray, "Expected: ARRAY actual: ARRAY>");
+ }
+
+ @Test
+ public void testPgJsonbArrayTryGetFloat64Array() {
+ Value value = Value.pgJsonbArray(Collections.singletonList("{}"));
+ assertThrowsWithMessage(
+ value::getFloat64Array, "Expected: ARRAY actual: ARRAY>");
+ }
+
@Test
public void bytesArray() {
ByteArray a = newByteArray("a");
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java
new file mode 100644
index 0000000000..1f3f59e96a
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ChecksumResultSetTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.connection;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.ByteArray;
+import com.google.cloud.Date;
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.AbortedDueToConcurrentModificationException;
+import com.google.cloud.spanner.AbortedException;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.ResultSets;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Struct;
+import com.google.cloud.spanner.Struct.Builder;
+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.AbstractStatementParser.ParsedStatement;
+import com.google.common.collect.ImmutableList;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ChecksumResultSetTest {
+ private static final Struct DIFFERENT_NON_NULL_VALUES =
+ Struct.newBuilder()
+ .set("boolVal")
+ .to(false)
+ .set("longVal")
+ .to(2 * 2)
+ .set("doubleVal")
+ .to(Value.float64(3.14d * 2d))
+ .set("bigDecimalVal")
+ .to(Value.numeric(BigDecimal.valueOf(123 * 2, 2)))
+ .set("pgNumericVal")
+ .to(Value.pgNumeric("2.46"))
+ .set("stringVal")
+ .to("testtest")
+ .set("jsonVal")
+ .to(Value.json("{\"color\":\"red\",\"value\":\"#ff0\"}"))
+ .set("pgJsonbVal")
+ .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#00f\"}"))
+ .set("byteVal")
+ .to(Value.bytes(ByteArray.copyFrom("bytes".getBytes(StandardCharsets.UTF_8))))
+ .set("timestamp")
+ .to(Timestamp.parseTimestamp("2022-08-04T11:20:00.123456789Z"))
+ .set("date")
+ .to(Date.fromYearMonthDay(2022, 8, 3))
+ .set("boolArray")
+ .to(Value.boolArray(Arrays.asList(Boolean.FALSE, null, Boolean.TRUE)))
+ .set("longArray")
+ .to(Value.int64Array(Arrays.asList(2L, null, 1L, 0L)))
+ .set("doubleArray")
+ .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d, 10.1d)))
+ .set("bigDecimalArray")
+ .to(Value.numericArray(Arrays.asList(BigDecimal.TEN, null, BigDecimal.ONE)))
+ .set("pgNumericArray")
+ .to(Value.pgNumericArray(Arrays.asList("10", null, "1", "NaN")))
+ .set("byteArray")
+ .to(
+ Value.bytesArray(
+ Arrays.asList(ByteArray.copyFrom("test2"), null, ByteArray.copyFrom("test1"))))
+ .set("timestampArray")
+ .to(
+ Value.timestampArray(
+ Arrays.asList(
+ Timestamp.parseTimestamp("2000-01-01T00:00:00Z"),
+ null,
+ Timestamp.parseTimestamp("2022-07-04T10:24:00.123456789Z"))))
+ .set("dateArray")
+ .to(
+ Value.dateArray(
+ Arrays.asList(Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-03"))))
+ .set("stringArray")
+ .to(Value.stringArray(Arrays.asList("test2", null, "test1")))
+ .set("jsonArray")
+ .to(Value.jsonArray(Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "[]")))
+ .set("pgJsonbArray")
+ .to(
+ Value.pgJsonbArray(
+ Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "[]")))
+ .build();
+
+ @Test
+ public void testRetry() {
+ Type type =
+ Type.struct(
+ Type.StructField.of("boolVal", Type.bool()),
+ Type.StructField.of("longVal", Type.int64()),
+ Type.StructField.of("doubleVal", Type.float64()),
+ Type.StructField.of("bigDecimalVal", Type.numeric()),
+ Type.StructField.of("pgNumericVal", Type.pgNumeric()),
+ Type.StructField.of("stringVal", Type.string()),
+ Type.StructField.of("jsonVal", Type.json()),
+ Type.StructField.of("pgJsonbVal", Type.pgJsonb()),
+ Type.StructField.of("byteVal", Type.bytes()),
+ Type.StructField.of("timestamp", Type.timestamp()),
+ Type.StructField.of("date", Type.date()),
+ Type.StructField.of("boolArray", Type.array(Type.bool())),
+ Type.StructField.of("longArray", Type.array(Type.int64())),
+ Type.StructField.of("doubleArray", Type.array(Type.float64())),
+ Type.StructField.of("bigDecimalArray", Type.array(Type.numeric())),
+ Type.StructField.of("pgNumericArray", Type.array(Type.pgNumeric())),
+ 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("jsonArray", Type.array(Type.json())),
+ Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())));
+ Struct rowNonNullValues =
+ Struct.newBuilder()
+ .set("boolVal")
+ .to(true)
+ .set("longVal")
+ .to(2)
+ .set("doubleVal")
+ .to(Value.float64(3.14d))
+ .set("bigDecimalVal")
+ .to(Value.numeric(BigDecimal.valueOf(123, 2)))
+ .set("pgNumericVal")
+ .to(Value.pgNumeric("1.23"))
+ .set("stringVal")
+ .to("test")
+ .set("jsonVal")
+ .to(Value.json("{\"color\":\"red\",\"value\":\"#f00\"}"))
+ .set("pgJsonbVal")
+ .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#f00\"}"))
+ .set("byteVal")
+ .to(Value.bytes(ByteArray.copyFrom("test".getBytes(StandardCharsets.UTF_8))))
+ .set("timestamp")
+ .to(Timestamp.parseTimestamp("2022-08-04T10:19:00.123456789Z"))
+ .set("date")
+ .to(Date.fromYearMonthDay(2022, 8, 4))
+ .set("boolArray")
+ .to(Value.boolArray(Arrays.asList(Boolean.TRUE, null, Boolean.FALSE)))
+ .set("longArray")
+ .to(Value.int64Array(Arrays.asList(1L, null, 2L)))
+ .set("doubleArray")
+ .to(Value.float64Array(Arrays.asList(3.14d, null, 6.6626d)))
+ .set("bigDecimalArray")
+ .to(Value.numericArray(Arrays.asList(BigDecimal.ONE, null, BigDecimal.TEN)))
+ .set("pgNumericArray")
+ .to(Value.pgNumericArray(Arrays.asList("1", null, "10")))
+ .set("byteArray")
+ .to(
+ Value.bytesArray(
+ Arrays.asList(ByteArray.copyFrom("test1"), null, ByteArray.copyFrom("test2"))))
+ .set("timestampArray")
+ .to(
+ Value.timestampArray(
+ Arrays.asList(
+ Timestamp.parseTimestamp("2000-01-01T00:00:00Z"),
+ null,
+ Timestamp.parseTimestamp("2022-08-04T10:24:00.123456789Z"))))
+ .set("dateArray")
+ .to(
+ Value.dateArray(
+ Arrays.asList(
+ Date.parseDate("2000-01-01"), null, Date.parseDate("2022-08-04"))))
+ .set("stringArray")
+ .to(Value.stringArray(Arrays.asList("test1", null, "test2")))
+ .set("jsonArray")
+ .to(
+ Value.jsonArray(
+ Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "{}")))
+ .set("pgJsonbArray")
+ .to(
+ Value.pgJsonbArray(
+ Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "{}")))
+ .build();
+ Struct rowNullValues =
+ Struct.newBuilder()
+ .set("boolVal")
+ .to((Boolean) null)
+ .set("longVal")
+ .to((Long) null)
+ .set("doubleVal")
+ .to((Double) null)
+ .set("bigDecimalVal")
+ .to((BigDecimal) null)
+ .set("pgNumericVal")
+ .to(Value.pgNumeric(null))
+ .set("stringVal")
+ .to((String) null)
+ .set("jsonVal")
+ .to(Value.json(null))
+ .set("pgJsonbVal")
+ .to(Value.pgJsonb(null))
+ .set("byteVal")
+ .to((ByteArray) null)
+ .set("timestamp")
+ .to((Timestamp) null)
+ .set("date")
+ .to((Date) null)
+ .set("boolArray")
+ .toBoolArray((Iterable) null)
+ .set("longArray")
+ .toInt64Array((Iterable) null)
+ .set("doubleArray")
+ .toFloat64Array((Iterable) null)
+ .set("bigDecimalArray")
+ .toNumericArray(null)
+ .set("pgNumericArray")
+ .toPgNumericArray(null)
+ .set("byteArray")
+ .toBytesArray(null)
+ .set("timestampArray")
+ .toTimestampArray(null)
+ .set("dateArray")
+ .toDateArray(null)
+ .set("stringArray")
+ .toStringArray(null)
+ .set("jsonArray")
+ .toJsonArray(null)
+ .set("pgJsonbArray")
+ .toPgJsonbArray(null)
+ .build();
+
+ ParsedStatement parsedStatement = mock(ParsedStatement.class);
+ Statement statement = Statement.of("select * from foo");
+ when(parsedStatement.getStatement()).thenReturn(statement);
+ AbortedException abortedException = mock(AbortedException.class);
+ ReadWriteTransaction transaction = mock(ReadWriteTransaction.class);
+ when(transaction.runWithRetry(any(Callable.class)))
+ .thenAnswer(invocationOnMock -> ((Callable>) invocationOnMock.getArgument(0)).call());
+ when(transaction.getStatementExecutor()).thenReturn(mock(StatementExecutor.class));
+
+ ResultSet queryResult =
+ ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, rowNullValues));
+ ChecksumResultSet resultSet =
+ new ChecksumResultSet(
+ transaction,
+ DirectExecuteResultSet.ofResultSet(queryResult),
+ parsedStatement,
+ AnalyzeMode.NONE);
+ assertTrue(resultSet.next());
+ assertTrue(resultSet.next());
+
+ // Ensure that retrying will return the same result.
+ ResultSet retryResult =
+ ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, rowNullValues));
+ when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE))
+ .thenReturn(retryResult);
+
+ // There have been no changes, so the retry should succeed.
+ resultSet.retry(abortedException);
+
+ // Change field value from one non-null value to another non-null value.
+ for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) {
+ Builder builder = Struct.newBuilder();
+ for (StructField field : rowNonNullValues.getType().getStructFields()) {
+ if (field.equals(fieldToChange)) {
+ builder.set(field.getName()).to(DIFFERENT_NON_NULL_VALUES.getValue(field.getName()));
+ } else {
+ builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName()));
+ }
+ }
+ ResultSet newRetryResult =
+ ResultSets.forRows(type, ImmutableList.of(builder.build(), rowNullValues));
+ when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE))
+ .thenReturn(newRetryResult);
+ // The query result has changed, so this should now fail.
+ assertThrows(
+ "Missing exception for " + fieldToChange.getName(),
+ AbortedDueToConcurrentModificationException.class,
+ () -> resultSet.retry(abortedException));
+ }
+
+ // Change field value from non-null value to null value.
+ for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) {
+ Builder builder = Struct.newBuilder();
+ for (StructField field : rowNonNullValues.getType().getStructFields()) {
+ if (field.equals(fieldToChange)) {
+ builder.set(field.getName()).to(rowNullValues.getValue(field.getName()));
+ } else {
+ builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName()));
+ }
+ }
+ ResultSet newRetryResult =
+ ResultSets.forRows(type, ImmutableList.of(builder.build(), rowNullValues));
+ when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE))
+ .thenReturn(newRetryResult);
+ // The query result has changed, so this should now fail.
+ assertThrows(
+ "Missing exception for " + fieldToChange.getName(),
+ AbortedDueToConcurrentModificationException.class,
+ () -> resultSet.retry(abortedException));
+ }
+
+ // Change field value from null value to non-null value.
+ for (StructField fieldToChange : rowNonNullValues.getType().getStructFields()) {
+ Builder builder = Struct.newBuilder();
+ for (StructField field : rowNullValues.getType().getStructFields()) {
+ if (field.equals(fieldToChange)) {
+ builder.set(field.getName()).to(rowNonNullValues.getValue(field.getName()));
+ } else {
+ builder.set(field.getName()).to(rowNullValues.getValue(field.getName()));
+ }
+ }
+ // In this case the modified values are in the second row that first only contained null
+ // values.
+ ResultSet newRetryResult =
+ ResultSets.forRows(type, ImmutableList.of(rowNonNullValues, builder.build()));
+ when(transaction.internalExecuteQuery(parsedStatement, AnalyzeMode.NONE))
+ .thenReturn(newRetryResult);
+ // The query result has changed, so this should now fail.
+ assertThrows(
+ "Missing exception for " + fieldToChange.getName(),
+ AbortedDueToConcurrentModificationException.class,
+ () -> resultSet.retry(abortedException));
+ }
+ }
+}
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 896055ee9a..094503cfbc 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
@@ -249,6 +249,15 @@ public void testValidMethodCall() throws IllegalArgumentException {
subject.getJsonList("test2");
verify(delegate).getJsonList("test2");
+ subject.getPgJsonb(0);
+ verify(delegate).getPgJsonb(0);
+ subject.getPgJsonb("test0");
+ verify(delegate).getPgJsonb("test0");
+ subject.getPgJsonbList(2);
+ verify(delegate).getPgJsonbList(2);
+ subject.getPgJsonbList("test2");
+ verify(delegate).getPgJsonbList("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 024032ab76..c3ac655a40 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
@@ -51,9 +51,12 @@ private static Type[] generateTypes(Dialect dialect) {
.build()
: Type.newBuilder().setCode(TypeCode.NUMERIC).build(),
Type.newBuilder().setCode(TypeCode.STRING).build(),
- Type.newBuilder()
- .setCode(dialect == Dialect.POSTGRESQL ? TypeCode.STRING : TypeCode.JSON)
- .build(),
+ dialect == Dialect.POSTGRESQL
+ ? Type.newBuilder()
+ .setCode(TypeCode.JSON)
+ .setTypeAnnotation(TypeAnnotationCode.PG_JSONB)
+ .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(),
@@ -85,8 +88,11 @@ private static Type[] generateTypes(Dialect dialect) {
Type.newBuilder()
.setCode(TypeCode.ARRAY)
.setArrayElementType(
- Type.newBuilder()
- .setCode(dialect == Dialect.POSTGRESQL ? TypeCode.STRING : TypeCode.JSON))
+ dialect == Dialect.POSTGRESQL
+ ? Type.newBuilder()
+ .setCode(TypeCode.JSON)
+ .setTypeAnnotation(TypeAnnotationCode.PG_JSONB)
+ : Type.newBuilder().setCode(TypeCode.JSON))
.build(),
Type.newBuilder()
.setCode(TypeCode.ARRAY)
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java
new file mode 100644
index 0000000000..74edbebb0b
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPgJsonbTest.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.it;
+
+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.Assume.assumeFalse;
+
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.testing.EmulatorSpannerHelper;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.NullValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+
+// TODO: Re-enable when jsonb is GA.
+@Ignore("Feature is not yet generally available")
+@Category(ParallelIntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITPgJsonbTest {
+
+ private static final Duration OPERATION_TIMEOUT = Duration.ofMinutes(5);
+
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+ private static RemoteSpannerHelper testHelper;
+ private static DatabaseAdminClient databaseAdminClient;
+ private static List databasesToDrop;
+ private static String projectId;
+ private static String instanceId;
+ private static String databaseId;
+ private DatabaseClient databaseClient;
+ private String tableName;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+ testHelper = env.getTestHelper();
+ databaseAdminClient = testHelper.getClient().getDatabaseAdminClient();
+ databasesToDrop = new ArrayList<>();
+ projectId = testHelper.getInstanceId().getProject();
+ instanceId = testHelper.getInstanceId().getInstance();
+ databaseId = testHelper.getUniqueDatabaseId();
+ final Database database =
+ databaseAdminClient
+ .newDatabaseBuilder(DatabaseId.of(projectId, instanceId, databaseId))
+ .setDialect(Dialect.POSTGRESQL)
+ .build();
+ databaseAdminClient
+ .createDatabase(database, Collections.emptyList())
+ .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+ databasesToDrop.add(database.getId());
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ if (databasesToDrop != null) {
+ for (DatabaseId id : databasesToDrop) {
+ try {
+ databaseAdminClient.dropDatabase(id.getInstanceId().getInstance(), id.getDatabase());
+ } catch (Exception e) {
+ System.err.println("Failed to drop database " + id + ", skipping...: " + e.getMessage());
+ }
+ }
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ databaseClient =
+ testHelper.getClient().getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
+ tableName = testHelper.getUniqueDatabaseId();
+ databaseAdminClient
+ .updateDatabaseDdl(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "CREATE TABLE \"" + tableName + "\" (id BIGINT PRIMARY KEY, col1 JSONB)"),
+ null)
+ .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testPgJsonbAsPrimaryKey() {
+ // JSONB is not allowed as a primary key.
+ ExecutionException executionException =
+ assertThrows(
+ ExecutionException.class,
+ () ->
+ databaseAdminClient
+ .updateDatabaseDdl(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "CREATE TABLE with_jsonb_pk (id jsonb primary key)"),
+ null)
+ .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS));
+ SpannerException spannerException =
+ SpannerExceptionFactory.asSpannerException(executionException.getCause());
+ assertEquals(ErrorCode.INVALID_ARGUMENT, spannerException.getErrorCode());
+ assertTrue(
+ spannerException.getMessage(),
+ spannerException
+ .getMessage()
+ .contains(
+ "Column with_jsonb_pk.id has type PG.JSONB, but is part of the primary key."));
+ }
+
+ @Test
+ public void testPgJsonbInSecondaryIndex() {
+ // JSONB is not allowed as a key in a secondary index.
+ ExecutionException executionException =
+ assertThrows(
+ ExecutionException.class,
+ () ->
+ databaseAdminClient
+ .updateDatabaseDdl(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "CREATE INDEX idx_jsonb on \"" + tableName + "\" (col1)"),
+ null)
+ .get(OPERATION_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS));
+ SpannerException spannerException =
+ SpannerExceptionFactory.asSpannerException(executionException.getCause());
+ assertEquals(ErrorCode.FAILED_PRECONDITION, spannerException.getErrorCode());
+ assertTrue(
+ spannerException.getMessage(),
+ spannerException
+ .getMessage()
+ .contains("Index idx_jsonb is defined on a column of unsupported type PG.JSONB."));
+ }
+
+ private static final String JSON_VALUE_1 = "{\"color\":\"red\",\"value\":\"#f00\"}";
+ private static final String JSON_VALUE_2 =
+ "["
+ + " {\"color\":\"red\",\"value\":\"#f00\"},"
+ + " {\"color\":\"green\",\"value\":\"#0f0\"},"
+ + " {\"color\":\"blue\",\"value\":\"#00f\"}"
+ + "]";
+
+ @Test
+ public void testLiteralPgJsonb() {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+ databaseClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.executeUpdate(
+ Statement.of(
+ "INSERT INTO "
+ + tableName
+ + " (id, col1) VALUES"
+ + " (1, '"
+ + JSON_VALUE_1
+ + "')"
+ + ", (2, '"
+ + JSON_VALUE_2
+ + "')"
+ + ", (3, '{}')"
+ + ", (4, '[]')"
+ + ", (5, null)"));
+ return null;
+ });
+
+ verifyContents();
+ }
+
+ @Test
+ public void testPgJsonbParameter() {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+ databaseClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.executeUpdate(
+ Statement.newBuilder(
+ "INSERT INTO "
+ + tableName
+ + " (id, col1) VALUES"
+ + " (1, $1)"
+ + ", (2, $2)"
+ + ", (3, $3)"
+ + ", (4, $4)"
+ + ", (5, $5)")
+ .bind("p1")
+ .to(Value.pgJsonb(JSON_VALUE_1))
+ .bind("p2")
+ .to(Value.pgJsonb(JSON_VALUE_2))
+ .bind("p3")
+ .to(Value.pgJsonb("{}"))
+ .bind("p4")
+ .to(Value.pgJsonb("[]"))
+ .bind("p5")
+ .to(Value.pgJsonb(null))
+ .build());
+ return null;
+ });
+
+ verifyContents();
+ }
+
+ @Ignore("Untyped jsonb parameters are not yet supported")
+ @Test
+ public void testPgJsonbUntypedParameter() {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+
+ // Verify that we can use Jsonb as an untyped parameter. This is especially important for
+ // PGAdapter and the JDBC driver, as these will often use untyped parameters.
+ databaseClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.executeUpdate(
+ Statement.newBuilder(
+ "INSERT INTO "
+ + tableName
+ + " (id, col1) VALUES"
+ + " (1, $1)"
+ + ", (2, $2)"
+ + ", (3, $3)"
+ + ", (4, $4)"
+ + ", (5, $5)")
+ .bind("p1")
+ .to(
+ Value.untyped(
+ com.google.protobuf.Value.newBuilder()
+ .setStringValue(JSON_VALUE_1)
+ .build()))
+ .bind("p2")
+ .to(
+ Value.untyped(
+ com.google.protobuf.Value.newBuilder()
+ .setStringValue(JSON_VALUE_2)
+ .build()))
+ .bind("p3")
+ .to(
+ Value.untyped(
+ com.google.protobuf.Value.newBuilder().setStringValue("{}").build()))
+ .bind("p4")
+ .to(
+ Value.untyped(
+ com.google.protobuf.Value.newBuilder().setStringValue("[]").build()))
+ .bind("p5")
+ .to(
+ Value.untyped(
+ com.google.protobuf.Value.newBuilder()
+ .setNullValue(NullValue.NULL_VALUE)
+ .build()))
+ .build());
+ return null;
+ });
+
+ verifyContents();
+ }
+
+ @Test
+ public void testMutationsWithPgJsonbAsString() {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+ databaseClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.buffer(
+ ImmutableList.of(
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(1)
+ .set("col1")
+ .to(JSON_VALUE_1)
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(2)
+ .set("col1")
+ .to(JSON_VALUE_2)
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(3)
+ .set("col1")
+ .to("{}")
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(4)
+ .set("col1")
+ .to("[]")
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(5)
+ .set("col1")
+ .to((String) null)
+ .build()));
+ return null;
+ });
+
+ verifyContents();
+ }
+
+ @Test
+ public void testMutationsWithPgJsonbAsValue() {
+ assumeFalse(
+ "PgJsonb is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator());
+ databaseClient
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ transaction.buffer(
+ ImmutableList.of(
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(1)
+ .set("col1")
+ .to(Value.pgJsonb(JSON_VALUE_1))
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(2)
+ .set("col1")
+ .to(Value.pgJsonb(JSON_VALUE_2))
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(3)
+ .set("col1")
+ .to(Value.pgJsonb("{}"))
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(4)
+ .set("col1")
+ .to(Value.pgJsonb("[]"))
+ .build(),
+ Mutation.newInsertBuilder(tableName)
+ .set("id")
+ .to(5)
+ .set("col1")
+ .to(Value.pgJsonb(null))
+ .build()));
+ return null;
+ });
+
+ verifyContents();
+ }
+
+ private void verifyContents() {
+ try (ResultSet resultSet =
+ databaseClient
+ .singleUse()
+ .executeQuery(Statement.of("SELECT * FROM " + tableName + " ORDER BY id"))) {
+
+ assertTrue(resultSet.next());
+ // Note: We do not use the JSON_VALUE_1 constant here, because the backend prettifies the
+ // value a little, which means that there is a small difference between what we insert and
+ // what we get back.
+ assertEquals("{\"color\": \"red\", \"value\": \"#f00\"}", resultSet.getPgJsonb("col1"));
+ assertEquals(
+ Value.pgJsonb("{\"color\": \"red\", \"value\": \"#f00\"}"), resultSet.getValue("col1"));
+
+ assertTrue(resultSet.next());
+ assertEquals(
+ "["
+ + "{\"color\": \"red\", \"value\": \"#f00\"}, "
+ + "{\"color\": \"green\", \"value\": \"#0f0\"}, "
+ + "{\"color\": \"blue\", \"value\": \"#00f\"}"
+ + "]",
+ resultSet.getPgJsonb("col1"));
+ assertEquals(
+ Value.pgJsonb(
+ "["
+ + "{\"color\": \"red\", \"value\": \"#f00\"}, "
+ + "{\"color\": \"green\", \"value\": \"#0f0\"}, "
+ + "{\"color\": \"blue\", \"value\": \"#00f\"}"
+ + "]"),
+ resultSet.getValue("col1"));
+
+ assertTrue(resultSet.next());
+ assertEquals("{}", resultSet.getPgJsonb("col1"));
+ assertEquals(Value.pgJsonb("{}"), resultSet.getValue("col1"));
+
+ assertTrue(resultSet.next());
+ assertEquals("[]", resultSet.getPgJsonb("col1"));
+ assertEquals(Value.pgJsonb("[]"), resultSet.getValue("col1"));
+
+ assertTrue(resultSet.next());
+ assertTrue(resultSet.isNull("col1"));
+
+ assertFalse(resultSet.next());
+ }
+ }
+}