expectedCodes, Object columnNameForError) {
+ Type actualType = getColumnType(columnIndex);
+ checkState(
+ expectedCodes.contains(actualType.getCode()),
+ "Column %s is not of correct type code: expected one of [%s] but was %s",
+ columnNameForError,
+ expectedCodes,
+ actualType);
+ checkNonNull(columnIndex, columnNameForError);
+ }
+
+ private void checkArrayElementType(
+ int columnIndex, List expectedCodes, Object columnNameForError) {
+ Type arrayElementType = getColumnType(columnIndex).getArrayElementType();
+ checkState(
+ expectedCodes.contains(arrayElementType.getCode()),
+ "Array element for Column %s is not of correct type code: expected one of [%s] but was %s",
+ columnNameForError,
+ expectedCodes,
+ Type.array(arrayElementType));
+ }
+
private void checkNonNullOfTypes(
int columnIndex,
List expectedTypes,
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
index 8372bb61fd3..f961a13845b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java
@@ -26,6 +26,7 @@
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
+import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse;
import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata;
@@ -424,7 +425,8 @@ OperationFuture updateDatabase(
*
* If an operation already exists with the given operation id, the operation will be resumed
* and the returned future will complete when the original operation finishes. See more
- * information in {@link com.google.cloud.spanner.spi.v1.GapicSpannerRpc#updateDatabaseDdl(String,
+ * information in {@link
+ * com.google.cloud.spanner.spi.v1.GapicSpannerRpc#updateDatabaseDdl(com.google.cloud.spanner.Database,
* Iterable, String)}
*
*
Example to update the database DDL.
@@ -449,6 +451,40 @@ OperationFuture updateDatabaseDdl(
@Nullable String operationId)
throws SpannerException;
+ /**
+ * Updates a database in a Cloud Spanner instance. Any proto descriptors that have been set for
+ * the {@link com.google.cloud.spanner.Database} instance will be included in the {@link
+ * com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest}.
+ *
+ * If an operation already exists with the given operation id, the operation will be resumed
+ * and the returned future will complete when the original operation finishes. See more
+ * information in {@link
+ * com.google.cloud.spanner.spi.v1.GapicSpannerRpc#updateDatabaseDdl(com.google.cloud.spanner.Database,
+ * Iterable, String)}
+ *
+ *
Example to update the database DDL with proto descriptors.
+ *
+ *
{@code
+ * Database dbInfo =
+ * dbClient
+ * .newDatabaseBuilder(DatabaseId.of("my_project_id", "my_instance_id", "my_database_id"))
+ * .setProtoDescriptors("com/google/cloud/spanner/descriptors.pb")
+ * .build();
+ * dbAdminClient.updateDatabaseDdl(dbInfo,
+ * Arrays.asList("ALTER TABLE Albums ADD COLUMN MarketingBudget INT64"),
+ * null).waitFor();
+ * }
+ *
+ * @param database Database object to set configuration options such as proto_descriptors.
+ * @param statements DDL statements to run while updating the database.
+ * @param operationId Operation id assigned to this operation. If null, system will autogenerate
+ * one. This must be unique within a database abd must be a valid identifier
+ * [a-zA-Z][a-zA-Z0-9_]*.
+ */
+ OperationFuture updateDatabaseDdl(
+ Database database, Iterable statements, @Nullable String operationId)
+ throws SpannerException;
+
/**
* Drops a Cloud Spanner database.
*
@@ -476,6 +512,23 @@ OperationFuture updateDatabaseDdl(
*/
List getDatabaseDdl(String instanceId, String databaseId);
+ /**
+ * Returns the GetDatabaseDdlResponse object of a Cloud Spanner database.
+ *
+ * Example to get GetDatabaseDdlResponse object of a Cloud Spanner database.
+ *
+ *
{@code
+ * String instanceId = my_instance_id;
+ * String databaseId = my_database_id;
+ * GetDatabaseDdlResponse response = dbAdminClient.getDatabaseDdl(instanceId, databaseId);
+ * }
+ *
+ * @param instanceId the id of the instance where the database was created.
+ * @param databaseId the id of the database.
+ * @return GetDatabaseDdlResponse object
+ */
+ GetDatabaseDdlResponse getDatabaseDdlResponse(String instanceId, String databaseId);
+
/**
* Returns the list of Cloud Spanner database in the given instance.
*
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
index 8a5d0d613a5..f53ae3bb0d3 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java
@@ -444,10 +444,20 @@ public OperationFuture updateDatabaseDdl(
final Iterable statements,
@Nullable String operationId)
throws SpannerException {
- final String dbName = getDatabaseName(instanceId, databaseId);
+
+ return updateDatabaseDdl(
+ newDatabaseBuilder(DatabaseId.of(projectId, instanceId, databaseId)).build(),
+ statements,
+ operationId);
+ }
+
+ @Override
+ public OperationFuture updateDatabaseDdl(
+ Database database, final Iterable statements, @Nullable String operationId)
+ throws SpannerException {
final String opId = operationId != null ? operationId : randomOperationId();
OperationFuture rawOperationFuture =
- rpc.updateDatabaseDdl(dbName, statements, opId);
+ rpc.updateDatabaseDdl(database, statements, opId);
return new OperationFutureImpl<>(
rawOperationFuture.getPollingFuture(),
rawOperationFuture.getInitialFuture(),
@@ -469,6 +479,11 @@ public void dropDatabase(String instanceId, String databaseId) throws SpannerExc
@Override
public List getDatabaseDdl(String instanceId, String databaseId) {
+ return getDatabaseDdlResponse(instanceId, databaseId).getStatementsList();
+ }
+
+ @Override
+ public GetDatabaseDdlResponse getDatabaseDdlResponse(String instanceId, String databaseId) {
String dbName = getDatabaseName(instanceId, databaseId);
return rpc.getDatabaseDdl(dbName);
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
index d231ef34e37..3f1a0f81eb0 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java
@@ -20,9 +20,13 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.encryption.CustomerManagedEncryption;
import com.google.common.base.Preconditions;
+import com.google.protobuf.ByteString;
import com.google.protobuf.FieldMask;
import com.google.spanner.admin.database.v1.Database.State;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.Objects;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/** Represents a Cloud Spanner database. */
@@ -97,6 +101,34 @@ protected Builder setReconciling(boolean reconciling) {
throw new UnsupportedOperationException("Unimplemented");
}
+ /**
+ * Optional for creating a new database.
+ *
+ * It is used by CREATE/ALTER PROTO BUNDLE statements which are part of DDL statements.
+ * Contains a protobuf-serialized [google.protobuf.FileDescriptorSet]. To generate a proto
+ * descriptors file run {@code protoc --include_imports
+ * --descriptor_set_out=DESCRIPTOR_OUTPUT_LOCATION LOCATION-OF-PROTO-FILES}
+ *
+ * @param protoDescriptors The proto descriptors input as byte[] to be used for the database.
+ * @return {@link Builder}
+ */
+ public abstract Builder setProtoDescriptors(@Nonnull byte[] protoDescriptors);
+
+ /**
+ * Optional for creating a new database.
+ *
+ *
It is used by CREATE/ALTER PROTO BUNDLE statements which are part of DDL statements.
+ * Contains a protobuf-serialized [google.protobuf.FileDescriptorSet]. To generate a proto
+ * descriptors file run {@code protoc --include_imports
+ * --descriptor_set_out=DESCRIPTOR_OUTPUT_LOCATION LOCATION-OF-PROTO-FILES}
+ *
+ * @param inputStream The proto descriptors input as InputStream to be used for the database.
+ * @return {@link Builder}
+ * @throws IOException if there is a problem reading the underlying stream.
+ */
+ public abstract Builder setProtoDescriptors(@Nonnull InputStream inputStream)
+ throws IOException;
+
abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto);
/** Builds the database from this builder. */
@@ -115,6 +147,7 @@ abstract static class BuilderImpl extends Builder {
private Dialect dialect = Dialect.GOOGLE_STANDARD_SQL;
private boolean dropProtectionEnabled;
private boolean reconciling;
+ private ByteString protoDescriptors;
private com.google.spanner.admin.database.v1.Database proto;
BuilderImpl(DatabaseId id) {
@@ -131,6 +164,7 @@ abstract static class BuilderImpl extends Builder {
this.encryptionConfig = other.encryptionConfig;
this.defaultLeader = other.defaultLeader;
this.dialect = other.dialect;
+ this.protoDescriptors = other.protoDescriptors;
this.proto = other.proto;
}
@@ -200,6 +234,20 @@ protected Builder setReconciling(boolean reconciling) {
return this;
}
+ @Override
+ public Builder setProtoDescriptors(@Nonnull byte[] protoDescriptors) {
+ Preconditions.checkNotNull(protoDescriptors);
+ this.protoDescriptors = ByteString.copyFrom(protoDescriptors);
+ return this;
+ }
+
+ @Override
+ public Builder setProtoDescriptors(@Nonnull InputStream inputStream) throws IOException {
+ Preconditions.checkNotNull(inputStream);
+ this.protoDescriptors = ByteString.readFrom(inputStream);
+ return this;
+ }
+
@Override
Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) {
this.proto = proto;
@@ -252,6 +300,8 @@ public com.google.spanner.admin.database.v1.Database.State toProto() {
private final Dialect dialect;
private final boolean dropProtectionEnabled;
private final boolean reconciling;
+
+ private final ByteString protoDescriptors;
private final com.google.spanner.admin.database.v1.Database proto;
public DatabaseInfo(DatabaseId id, State state) {
@@ -266,6 +316,7 @@ public DatabaseInfo(DatabaseId id, State state) {
this.dialect = null;
this.dropProtectionEnabled = false;
this.reconciling = false;
+ this.protoDescriptors = null;
this.proto = null;
}
@@ -281,6 +332,7 @@ public DatabaseInfo(DatabaseId id, State state) {
this.dialect = builder.dialect;
this.dropProtectionEnabled = builder.dropProtectionEnabled;
this.reconciling = builder.reconciling;
+ this.protoDescriptors = builder.protoDescriptors;
this.proto = builder.proto;
}
@@ -357,6 +409,10 @@ public boolean getReconciling() {
return reconciling;
}
+ public ByteString getProtoDescriptors() {
+ return protoDescriptors;
+ }
+
/** Returns the raw proto instance that was used to construct this {@link Database}. */
public @Nullable com.google.spanner.admin.database.v1.Database getProto() {
return proto;
@@ -381,7 +437,8 @@ public boolean equals(Object o) {
&& Objects.equals(defaultLeader, that.defaultLeader)
&& Objects.equals(dialect, that.dialect)
&& Objects.equals(dropProtectionEnabled, that.dropProtectionEnabled)
- && Objects.equals(reconciling, that.reconciling);
+ && Objects.equals(reconciling, that.reconciling)
+ && Objects.equals(protoDescriptors, that.protoDescriptors);
}
@Override
@@ -397,13 +454,14 @@ public int hashCode() {
defaultLeader,
dialect,
dropProtectionEnabled,
- reconciling);
+ reconciling,
+ protoDescriptors);
}
@Override
public String toString() {
return String.format(
- "Database[%s, %s, %s, %s, %s, %s, %s, %s, %s %s %s]",
+ "Database[%s, %s, %s, %s, %s, %s, %s, %s, %s %s %s %s]",
id.getName(),
state,
createTime,
@@ -414,6 +472,7 @@ public String toString() {
defaultLeader,
dialect,
dropProtectionEnabled,
- reconciling);
+ reconciling,
+ protoDescriptors);
}
}
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 2a85006fa95..97c39c00a8d 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
@@ -22,8 +22,11 @@
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import java.math.BigDecimal;
import java.util.List;
+import java.util.function.Function;
/** Forwarding implements of StructReader */
public class ForwardingStructReader implements StructReader {
@@ -370,6 +373,32 @@ public List getDateList(String columnName) {
return delegate.get().getDateList(columnName);
}
+ @Override
+ public List getProtoMessageList(int columnIndex, T message) {
+ checkValidState();
+ return delegate.get().getProtoMessageList(columnIndex, message);
+ }
+
+ @Override
+ public List getProtoMessageList(String columnName, T message) {
+ checkValidState();
+ return delegate.get().getProtoMessageList(columnName, message);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ int columnIndex, Function method) {
+ checkValidState();
+ return delegate.get().getProtoEnumList(columnIndex, method);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ String columnName, Function method) {
+ checkValidState();
+ return delegate.get().getProtoEnumList(columnName, method);
+ }
+
@Override
public List getStructList(int columnIndex) {
checkValidState();
@@ -382,6 +411,32 @@ public List getStructList(String columnName) {
return delegate.get().getStructList(columnName);
}
+ @Override
+ public T getProtoMessage(int columnIndex, T message) {
+ checkValidState();
+ return delegate.get().getProtoMessage(columnIndex, message);
+ }
+
+ @Override
+ public T getProtoMessage(String columnName, T message) {
+ checkValidState();
+ return delegate.get().getProtoMessage(columnName, message);
+ }
+
+ @Override
+ public T getProtoEnum(
+ int columnIndex, Function method) {
+ checkValidState();
+ return delegate.get().getProtoEnum(columnIndex, method);
+ }
+
+ @Override
+ public T getProtoEnum(
+ String columnName, Function method) {
+ checkValidState();
+ return delegate.get().getProtoEnum(columnName, method);
+ }
+
@Override
public Value getValue(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 15d4e995bf0..3467052605a 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
@@ -24,6 +24,7 @@
import com.google.common.base.Joiner;
import com.google.protobuf.ListValue;
import com.google.protobuf.NullValue;
+import com.google.protobuf.ProtocolMessageEnum;
import com.google.protobuf.Value;
import java.io.Serializable;
import java.math.BigDecimal;
@@ -141,6 +142,11 @@ public Builder append(@Nullable BigDecimal value) {
buffer.add(value);
return this;
}
+ /** Appends a {@code ENUM} value to the key. */
+ public Builder append(@Nullable ProtocolMessageEnum value) {
+ buffer.add(value);
+ return this;
+ }
/** Appends a {@code STRING} value to the key. */
public Builder append(@Nullable String value) {
buffer.add(value);
@@ -192,6 +198,8 @@ public Builder appendObject(@Nullable Object value) {
append((Timestamp) value);
} else if (value instanceof Date) {
append((Date) value);
+ } else if (value instanceof ProtocolMessageEnum) {
+ append((ProtocolMessageEnum) value);
} else {
throw new IllegalArgumentException(
"Unsupported type ["
@@ -300,6 +308,10 @@ ListValue toProto() {
builder.addValuesBuilder().setStringValue(part.toString());
} else if (part instanceof Date) {
builder.addValuesBuilder().setStringValue(part.toString());
+ } else if (part instanceof ProtocolMessageEnum) {
+ builder
+ .addValuesBuilder()
+ .setStringValue(Long.toString(((ProtocolMessageEnum) part).getNumber()));
} else {
throw new AssertionError("Illegal key part: " + part.getClass());
}
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 fa054ba0cda..d55d4091b9f 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
@@ -29,10 +29,13 @@
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import com.google.spanner.v1.ResultSetMetadata;
import com.google.spanner.v1.ResultSetStats;
import java.math.BigDecimal;
import java.util.List;
+import java.util.function.Function;
/** Utility methods for working with {@link com.google.cloud.spanner.ResultSet}. */
public final class ResultSets {
@@ -300,6 +303,28 @@ public Date getDate(String columnName) {
return getCurrentRowAsStruct().getDate(columnName);
}
+ @Override
+ public T getProtoMessage(int columnIndex, T message) {
+ return getCurrentRowAsStruct().getProtoMessage(columnIndex, message);
+ }
+
+ @Override
+ public T getProtoMessage(String columnName, T message) {
+ return getCurrentRowAsStruct().getProtoMessage(columnName, message);
+ }
+
+ @Override
+ public T getProtoEnum(
+ int columnIndex, Function method) {
+ return getCurrentRowAsStruct().getProtoEnum(columnIndex, method);
+ }
+
+ @Override
+ public T getProtoEnum(
+ String columnName, Function method) {
+ return getCurrentRowAsStruct().getProtoEnum(columnName, method);
+ }
+
@Override
public Value getValue(int columnIndex) {
return getCurrentRowAsStruct().getValue(columnIndex);
@@ -440,6 +465,28 @@ public List getDateList(String columnName) {
return getCurrentRowAsStruct().getDateList(columnName);
}
+ @Override
+ public List getProtoMessageList(int columnIndex, T message) {
+ return getCurrentRowAsStruct().getProtoMessageList(columnIndex, message);
+ }
+
+ @Override
+ public List getProtoMessageList(String columnName, T message) {
+ return getCurrentRowAsStruct().getProtoMessageList(columnName, message);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ int columnIndex, Function method) {
+ return getCurrentRowAsStruct().getProtoEnumList(columnIndex, method);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ String columnName, Function method) {
+ return getCurrentRowAsStruct().getProtoEnumList(columnName, method);
+ }
+
@Override
public List getStructList(int columnIndex) {
return getCurrentRowAsStruct().getStructList(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 48c989d145e..40c30148d0e 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
@@ -28,11 +28,14 @@
import com.google.common.primitives.Booleans;
import com.google.common.primitives.Doubles;
import com.google.common.primitives.Longs;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import javax.annotation.concurrent.Immutable;
/**
@@ -217,6 +220,17 @@ protected Date getDateInternal(int columnIndex) {
return values.get(columnIndex).getDate();
}
+ @Override
+ protected T getProtoMessageInternal(int columnIndex, T message) {
+ return values.get(columnIndex).getProtoMessage(message);
+ }
+
+ @Override
+ protected T getProtoEnumInternal(
+ int columnIndex, Function method) {
+ return values.get(columnIndex).getProtoEnum(method);
+ }
+
@Override
protected Value getValueInternal(int columnIndex) {
return values.get(columnIndex);
@@ -287,6 +301,18 @@ protected List getTimestampListInternal(int columnIndex) {
return values.get(columnIndex).getTimestampArray();
}
+ @Override
+ protected List getProtoMessageListInternal(
+ int columnIndex, T message) {
+ return values.get(columnIndex).getProtoMessageArray(message);
+ }
+
+ @Override
+ protected List getProtoEnumListInternal(
+ int columnIndex, Function method) {
+ return values.get(columnIndex).getProtoEnumArray(method);
+ }
+
@Override
protected List getDateListInternal(int columnIndex) {
return values.get(columnIndex).getDateArray();
@@ -354,6 +380,7 @@ private Object getAsObject(int columnIndex) {
case BOOL:
return getBooleanInternal(columnIndex);
case INT64:
+ case ENUM:
return getLongInternal(columnIndex);
case FLOAT64:
return getDoubleInternal(columnIndex);
@@ -368,6 +395,7 @@ private Object getAsObject(int columnIndex) {
case PG_JSONB:
return getPgJsonbInternal(columnIndex);
case BYTES:
+ case PROTO:
return getBytesInternal(columnIndex);
case TIMESTAMP:
return getTimestampInternal(columnIndex);
@@ -380,6 +408,7 @@ private Object getAsObject(int columnIndex) {
case BOOL:
return getBooleanListInternal(columnIndex);
case INT64:
+ case ENUM:
return getLongListInternal(columnIndex);
case FLOAT64:
return getDoubleListInternal(columnIndex);
@@ -394,6 +423,7 @@ private Object getAsObject(int columnIndex) {
case PG_JSONB:
return getPgJsonbListInternal(columnIndex);
case BYTES:
+ case PROTO:
return getBytesListInternal(columnIndex);
case TIMESTAMP:
return getTimestampListInternal(columnIndex);
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 ad085ca2dcc..fd8cb77f397 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
@@ -19,8 +19,11 @@
import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.Timestamp;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import java.math.BigDecimal;
import java.util.List;
+import java.util.function.Function;
/**
* A base interface for reading the fields of a {@code STRUCT}. The Cloud Spanner yields {@code
@@ -188,6 +191,60 @@ default String getPgJsonb(String columnName) {
throw new UnsupportedOperationException("method should be overwritten");
}
+ /**
+ * To get the proto message of generic type {@code T} from Struct.
+ *
+ * @param columnIndex Index of the column.
+ * @param message Proto message object. Message can't be null as it's internally used to find the
+ * type of proto. Use @code{MyProtoClass.getDefaultInstance()}. @see getDefaultInstance()
+ * @return The value of a non-{@code NULL} column with type {@link Type#proto(String)} ()}.
+ */
+ default T getProtoMessage(int columnIndex, T message) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto message of type {@code T} from Struct.
+ *
+ * @param columnName Name of the column.
+ * @param message Proto message object. Message can't be null as it's internally used to find the
+ * type of proto. Use @code{MyProtoClass.getDefaultInstance()}. @see getDefaultInstance()
+ * @return The value of a non-{@code NULL} column with type {@link Type#proto(String)} ()}.
+ */
+ default T getProtoMessage(String columnName, T message) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto enum of type {@code T} from Struct.
+ *
+ * @param columnIndex Index of the column.
+ * @param method A function that takes enum integer constant as argument and returns the enum. Use
+ * method @code{forNumber} from generated enum class (eg: MyProtoEnum::forNumber). @see forNumber
+ * @return The value of a non-{@code NULL} column with type {@link Type#protoEnum(String)} ()}.
+ */
+ default T getProtoEnum(
+ int columnIndex, Function method) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto enum of type {@code T} from Struct.
+ *
+ * @param columnName Name of the column.
+ * @param method A function that takes enum integer constant as argument and returns the enum. Use
+ * method @code{forNumber} from generated enum class (eg: MyProtoEnum::forNumber). @see forNumber
+ * @return The value of a non-{@code NULL} column with type {@link Type#protoEnum(String)} ()}.
+ */
+ default T getProtoEnum(
+ String columnName, Function method) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
/**
* @param columnIndex index of the column
* @return the value of a non-{@code NULL} column with type {@link Type#bytes()}.
@@ -408,6 +465,64 @@ default List getPgJsonbList(String columnName) {
throw new UnsupportedOperationException("method should be overwritten");
};
+ /**
+ * To get the proto message of generic type {@code T} from Struct.
+ *
+ * @param columnIndex Index of the column.
+ * @param message Proto message object. Message can't be null as it's internally used to find the
+ * type of proto. Use @code{MyProtoClass.getDefaultInstance()}. @see getDefaultInstance()
+ * @return The value of a non-{@code NULL} column with type {@code
+ * Type.array(Type.proto(String))}.
+ */
+ default List getProtoMessageList(int columnIndex, T message) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto message of type {@code T} from Struct.
+ *
+ * @param columnName Name of the column.
+ * @param message Proto message object. Message can't be null as it's internally used to find the
+ * type of proto. Use @code{MyProtoClass.getDefaultInstance()}. @see getDefaultInstance()
+ * @return The value of a non-{@code NULL} column with type {@code
+ * Type.array(Type.proto(String))}.
+ */
+ default List getProtoMessageList(String columnName, T message) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto enum of type {@code T} from Struct.
+ *
+ * @param columnIndex Index of the column.
+ * @param method A function that takes enum integer constant as argument and returns the enum. Use
+ * method @code{forNumber} from generated enum class (eg: MyProtoEnum::forNumber). @see forNumber
+ * @return The value of a non-{@code NULL} column with type {@code
+ * Type.array(Type.protoEnum(String))}.
+ */
+ default List getProtoEnumList(
+ int columnIndex, Function method) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
+ /**
+ * To get the proto enum list of type {@code T} from Struct.
+ *
+ * @param columnName Name of the column.
+ * @param method A function that takes enum integer constant as argument and returns the enum. Use
+ * method @code{forNumber} from generated enum class (eg: MyProtoEnum::forNumber). @see forNumber
+ * @return The value of a non-{@code NULL} column with type {@code
+ * Type.array(Type.protoEnum(String))}.
+ */
+ default List getProtoEnumList(
+ String columnName, Function method) {
+ throw new UnsupportedOperationException("method should be overwritten");
+ }
+
/**
* @param columnIndex index of the column
* @return the value of a non-{@code NULL} column with type {@code Type.array(Type.bytes())}. The
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 37262cc6b3f..348db5d04ae 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
@@ -127,6 +127,24 @@ public static Type pgJsonb() {
return TYPE_PG_JSONB;
}
+ /**
+ * To get the descriptor for the {@code PROTO} type.
+ *
+ * @param protoTypeFqn Proto fully qualified name (ex: "spanner.examples.music.SingerInfo").
+ */
+ public static Type proto(String protoTypeFqn) {
+ return new Type(Code.PROTO, protoTypeFqn);
+ }
+
+ /**
+ * To get the descriptor for the {@code ENUM} type.
+ *
+ * @param protoTypeFqn Proto ENUM fully qualified name (ex: "spanner.examples.music.Genre")
+ */
+ public static Type protoEnum(String protoTypeFqn) {
+ return new Type(Code.ENUM, protoTypeFqn);
+ }
+
/** Returns the descriptor for the {@code BYTES} type: a variable-length byte string. */
public static Type bytes() {
return TYPE_BYTES;
@@ -199,6 +217,7 @@ public static Type struct(StructField... fields) {
private final Code code;
private final Type arrayElementType;
private final ImmutableList structFields;
+ private String protoTypeFqn;
/**
* Map of field name to field index. Ambiguous names are indexed to {@link #AMBIGUOUS_FIELD}. The
@@ -232,6 +251,11 @@ private Type(
this.structFields = structFields;
}
+ private Type(Code code, @Nonnull String protoTypeFqn) {
+ this(code, null, null);
+ this.protoTypeFqn = protoTypeFqn;
+ }
+
/** Enumerates the categories of types. */
public enum Code {
UNRECOGNIZED(TypeCode.UNRECOGNIZED, "unknown"),
@@ -243,6 +267,8 @@ public enum Code {
STRING(TypeCode.STRING, "character varying"),
JSON(TypeCode.JSON, "unknown"),
PG_JSONB(TypeCode.JSON, "jsonb", TypeAnnotationCode.PG_JSONB),
+ PROTO(TypeCode.PROTO, "proto"),
+ ENUM(TypeCode.ENUM, "enum"),
BYTES(TypeCode.BYTES, "bytea"),
TIMESTAMP(TypeCode.TIMESTAMP, "timestamp with time zone"),
DATE(TypeCode.DATE, "date"),
@@ -373,6 +399,17 @@ public List getStructFields() {
return structFields;
}
+ /**
+ * Returns the full package name for elements of this {@code Proto or @code Enum} type.
+ *
+ * @throws IllegalStateException if {@code code() != Code.PROTO or code() != Code.ENUM}
+ */
+ public String getProtoTypeFqn() {
+ Preconditions.checkState(
+ (code == Code.PROTO || code == Code.ENUM), "Illegal call for non-Proto type");
+ return protoTypeFqn;
+ }
+
/**
* Returns the index of the field named {@code fieldName} in this {@code STRUCT} type.
*
@@ -488,7 +525,8 @@ public boolean equals(Object o) {
}
return code == that.code
&& Objects.equals(arrayElementType, that.arrayElementType)
- && Objects.equals(structFields, that.structFields);
+ && Objects.equals(structFields, that.structFields)
+ && Objects.equals(protoTypeFqn, that.protoTypeFqn);
}
@Override
@@ -513,7 +551,10 @@ com.google.spanner.v1.Type toProto() {
for (StructField field : structFields) {
fields.addFieldsBuilder().setName(field.getName()).setType(field.getType().toProto());
}
+ } else if (code == Code.PROTO || code == Code.ENUM) {
+ proto.setProtoTypeFqn(protoTypeFqn);
}
+
return proto.build();
}
@@ -542,6 +583,10 @@ static Type fromProto(com.google.spanner.v1.Type proto) {
return timestamp();
case DATE:
return date();
+ case PROTO:
+ return proto(proto.getProtoTypeFqn());
+ case ENUM:
+ return protoEnum(proto.getProtoTypeFqn());
case ARRAY:
checkArgument(
proto.hasArrayElementType(),
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 c30847d6fe3..7cc7b675780 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
@@ -26,19 +26,28 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.io.CharSource;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumDescriptor;
+import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.ListValue;
import com.google.protobuf.NullValue;
+import com.google.protobuf.ProtocolMessageEnum;
import com.google.protobuf.Value.KindCase;
import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -232,7 +241,87 @@ public static Value pgJsonb(@Nullable String v) {
}
/**
- * Returns a {@code BYTES} value.
+ * Return a {@code PROTO} value for not null proto messages.
+ *
+ * @param v Not null Proto message.
+ */
+ public static Value protoMessage(AbstractMessage v) {
+ Preconditions.checkNotNull(
+ v, "Use protoMessage((ByteArray) null, MyProtoClass.getDescriptor()) for null values.");
+ return protoMessage(
+ ByteArray.copyFrom(v.toByteArray()), v.getDescriptorForType().getFullName());
+ }
+
+ /**
+ * Return a {@code PROTO} value
+ *
+ * @param v Serialized Proto Array, which may be null.
+ * @param protoTypeFqn Fully qualified name of proto representing the proto definition. Use static
+ * method from proto class {@code MyProtoClass.getDescriptor().getFullName()}
+ */
+ public static Value protoMessage(@Nullable ByteArray v, String protoTypeFqn) {
+ return new ProtoMessageImpl(v == null, v, protoTypeFqn);
+ }
+
+ /**
+ * Return a {@code PROTO} value
+ *
+ * @param v Serialized Proto Array, which may be null.
+ * @param descriptor Proto Type Descriptor, use static method from proto class {@code
+ * MyProtoClass.getDescriptor()}.
+ */
+ public static Value protoMessage(@Nullable ByteArray v, Descriptor descriptor) {
+ Preconditions.checkNotNull(descriptor, "descriptor can't be null.");
+ return protoMessage(v, descriptor.getFullName());
+ }
+
+ /**
+ * Return a {@code ENUM} value for not null proto messages.
+ *
+ * @param v Proto Enum, which may be null.
+ */
+ public static Value protoEnum(ProtocolMessageEnum v) {
+ Preconditions.checkNotNull(
+ v, "Use protoEnum((Long) null, MyProtoEnum.getDescriptor()) for null values.");
+ return protoEnum(v.getNumber(), v.getDescriptorForType().getFullName());
+ }
+
+ /**
+ * Return a {@code ENUM} value.
+ *
+ * @param v Enum non-primitive Integer constant.
+ * @param protoTypeFqn Fully qualified name of proto representing the enum definition. Use static
+ * method from proto class {@code MyProtoEnum.getDescriptor().getFullName()}
+ */
+ public static Value protoEnum(@Nullable Long v, String protoTypeFqn) {
+ return new ProtoEnumImpl(v == null, v, protoTypeFqn);
+ }
+
+ /**
+ * Return a {@code ENUM} value.
+ *
+ * @param v Enum non-primitive Integer constant.
+ * @param enumDescriptor Enum Type Descriptor. Use static method from proto class {@code *
+ * MyProtoEnum.getDescriptor()}.
+ */
+ public static Value protoEnum(@Nullable Long v, EnumDescriptor enumDescriptor) {
+ Preconditions.checkNotNull(enumDescriptor, "descriptor can't be null.");
+ return protoEnum(v, enumDescriptor.getFullName());
+ }
+
+ /**
+ * Return a {@code ENUM} value.
+ *
+ * @param v Enum integer primitive constant.
+ * @param protoTypeFqn Fully qualified name of proto representing the enum definition. Use static
+ * method from proto class {@code MyProtoEnum.getDescriptor().getFullName()}
+ */
+ public static Value protoEnum(long v, String protoTypeFqn) {
+ return new ProtoEnumImpl(false, v, protoTypeFqn);
+ }
+
+ /**
+ * e Returns a {@code BYTES} value. Returns a {@code BYTES} value.
*
* @param v the value, which may be null
*/
@@ -450,6 +539,85 @@ public static Value pgJsonbArray(@Nullable Iterable v) {
return new PgJsonbArrayImpl(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}.
+ * @param descriptor Proto Type Descriptor, use static method from proto class {@code
+ * MyProtoClass.getDescriptor()}.
+ */
+ public static Value protoMessageArray(
+ @Nullable Iterable v, Descriptor descriptor) {
+ if (v == null) {
+ return new ProtoMessageArrayImpl(true, null, descriptor.getFullName());
+ }
+
+ List serializedArray = new ArrayList<>();
+ v.forEach(
+ (message) -> {
+ if (message != null) {
+ serializedArray.add(ByteArray.copyFrom(message.toByteArray()));
+ } else {
+ serializedArray.add(null);
+ }
+ });
+
+ return new ProtoMessageArrayImpl(false, serializedArray, descriptor.getFullName());
+ }
+
+ /**
+ * 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}.
+ * @param protoTypeFqn Fully qualified name of proto representing the proto definition. Use static
+ * method from proto class {@code MyProtoClass.getDescriptor().getFullName()}
+ */
+ public static Value protoMessageArray(@Nullable Iterable v, String protoTypeFqn) {
+ return new ProtoMessageArrayImpl(
+ v == null, v != null ? immutableCopyOf(v) : null, protoTypeFqn);
+ }
+
+ /**
+ * 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}.
+ * @param descriptor Proto Type Descriptor, use static method from proto class {@code
+ * MyProtoClass.getDescriptor()}.
+ */
+ public static Value protoEnumArray(
+ @Nullable Iterable v, EnumDescriptor descriptor) {
+ if (v == null) {
+ return new ProtoEnumArrayImpl(true, null, descriptor.getFullName());
+ }
+
+ List enumConstValues = new ArrayList<>();
+ v.forEach(
+ (protoEnum) -> {
+ if (protoEnum != null) {
+ enumConstValues.add((long) protoEnum.getNumber());
+ } else {
+ enumConstValues.add(null);
+ }
+ });
+
+ return new ProtoEnumArrayImpl(false, enumConstValues, descriptor.getFullName());
+ }
+
+ /**
+ * 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}.
+ * @param protoTypeFqn Fully qualified name of proto representing the enum definition. Use static
+ * method from proto class {@code MyProtoEnum.getDescriptor().getFullName()}
+ */
+ public static Value protoEnumArray(@Nullable Iterable v, String protoTypeFqn) {
+ return new ProtoEnumArrayImpl(v == null, v != null ? immutableCopyOf(v) : null, protoTypeFqn);
+ }
+
/**
* Returns an {@code ARRAY} value.
*
@@ -600,6 +768,25 @@ public String getPgJsonb() {
throw new UnsupportedOperationException("Not implemented");
}
+ /**
+ * Returns the value of a {@code PROTO}-typed instance.
+ *
+ * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+ */
+ public T getProtoMessage(T m) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ /**
+ * Returns the value of a {@code ENUM}-typed instance.
+ *
+ * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+ */
+ public T getProtoEnum(
+ Function method) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
/**
* Returns the value of a {@code BYTES}-typed instance.
*
@@ -692,6 +879,27 @@ 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.
+ *
+ * @throws IllegalStateException if {@code isNull()} or the value is not of the expected type
+ */
+ public List getProtoMessageArray(T m) {
+ 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 getProtoEnumArray(
+ Function method) {
+ 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.
@@ -1254,6 +1462,15 @@ public long getInt64() {
return value;
}
+ @Override
+ public T getProtoEnum(
+ Function method) {
+ Preconditions.checkNotNull(
+ method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value.");
+ checkNotNull();
+ return (T) method.apply((int) value);
+ }
+
@Override
com.google.protobuf.Value valueToProto() {
return com.google.protobuf.Value.newBuilder().setStringValue(Long.toString(value)).build();
@@ -1463,6 +1680,27 @@ public ByteArray getBytes() {
return value.getByteArray();
}
+ @Override
+ public T getProtoMessage(T m) {
+ Preconditions.checkNotNull(
+ m,
+ "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value.");
+ checkNotNull();
+ try {
+ return (T)
+ m.toBuilder()
+ .mergeFrom(
+ Base64.getDecoder()
+ .wrap(
+ CharSource.wrap(value.getBase64String())
+ .asByteSource(StandardCharsets.UTF_8)
+ .openStream()))
+ .build();
+ } catch (IOException ioException) {
+ throw SpannerExceptionFactory.asSpannerException(ioException);
+ }
+ }
+
@Override
com.google.protobuf.Value valueToProto() {
return com.google.protobuf.Value.newBuilder().setStringValue(value.getBase64String()).build();
@@ -1480,6 +1718,75 @@ void valueToString(StringBuilder b) {
}
}
+ private static class ProtoMessageImpl extends AbstractObjectValue {
+
+ private ProtoMessageImpl(boolean isNull, ByteArray serializedProtoArray, String protoTypeFqn) {
+ super(isNull, Type.proto(protoTypeFqn), serializedProtoArray);
+ }
+
+ @Override
+ public ByteArray getBytes() {
+ checkNotNull();
+ return value;
+ }
+
+ @Override
+ public T getProtoMessage(T m) {
+ Preconditions.checkNotNull(
+ m,
+ "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value.");
+ checkNotNull();
+ try {
+ return (T) m.toBuilder().mergeFrom(value.toByteArray()).build();
+ } catch (InvalidProtocolBufferException e) {
+ throw SpannerExceptionFactory.asSpannerException(e);
+ }
+ }
+
+ @Override
+ com.google.protobuf.Value valueToProto() {
+ String base64EncodedString = value.toBase64();
+ return com.google.protobuf.Value.newBuilder().setStringValue(base64EncodedString).build();
+ }
+
+ @Override
+ void valueToString(StringBuilder b) {
+ b.append(value.toString());
+ }
+ }
+
+ private static class ProtoEnumImpl extends AbstractObjectValue {
+
+ private ProtoEnumImpl(boolean isNull, Long enumValue, String protoTypeFqn) {
+ super(isNull, Type.protoEnum(protoTypeFqn), enumValue);
+ }
+
+ @Override
+ public long getInt64() {
+ checkNotNull();
+ return value;
+ }
+
+ @Override
+ public T getProtoEnum(
+ Function method) {
+ Preconditions.checkNotNull(
+ method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value.");
+ checkNotNull();
+ return (T) method.apply(value.intValue());
+ }
+
+ @Override
+ void valueToString(StringBuilder b) {
+ b.append(value.toString());
+ }
+
+ @Override
+ com.google.protobuf.Value valueToProto() {
+ return com.google.protobuf.Value.newBuilder().setStringValue(Long.toString(value)).build();
+ }
+ }
+
private static class TimestampImpl extends AbstractObjectValue {
private static final String COMMIT_TIMESTAMP_STRING = "spanner.commit_timestamp()";
@@ -1742,6 +2049,24 @@ public List getInt64Array() {
return getArray();
}
+ @Override
+ public List getProtoEnumArray(
+ Function method) {
+ Preconditions.checkNotNull(
+ method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value.");
+ checkNotNull();
+
+ List protoEnumList = new ArrayList<>();
+ for (Long enumIntValue : values) {
+ if (enumIntValue == null) {
+ protoEnumList.add(null);
+ } else {
+ protoEnumList.add((T) method.apply(enumIntValue.intValue()));
+ }
+ }
+ return protoEnumList;
+ }
+
@Override
boolean valueEquals(Value v) {
Int64ArrayImpl that = (Int64ArrayImpl) v;
@@ -1962,6 +2287,36 @@ public List getBytesArray() {
}
}
+ @Override
+ public List getProtoMessageArray(T m) {
+ Preconditions.checkNotNull(
+ m,
+ "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value.");
+ checkNotNull();
+ try {
+ List protoMessagesList = new ArrayList<>(value.size());
+ for (LazyByteArray protoMessageBytes : value) {
+ if (protoMessageBytes == null) {
+ protoMessagesList.add(null);
+ } else {
+ protoMessagesList.add(
+ (T)
+ m.toBuilder()
+ .mergeFrom(
+ Base64.getDecoder()
+ .wrap(
+ CharSource.wrap(protoMessageBytes.getBase64String())
+ .asByteSource(StandardCharsets.UTF_8)
+ .openStream()))
+ .build());
+ }
+ }
+ return protoMessagesList;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Override
String elementToString(LazyByteArray element) {
return element.getBase64String();
@@ -1991,6 +2346,91 @@ void appendElement(StringBuilder b, Timestamp element) {
}
}
+ private static class ProtoMessageArrayImpl extends AbstractArrayValue {
+
+ private ProtoMessageArrayImpl(
+ boolean isNull, @Nullable List values, String protoTypeFqn) {
+ super(isNull, Type.proto(protoTypeFqn), values);
+ }
+
+ @Override
+ public List getBytesArray() {
+ return value;
+ }
+
+ @Override
+ public List getProtoMessageArray(T m) {
+ Preconditions.checkNotNull(
+ m,
+ "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value.");
+ checkNotNull();
+ try {
+ List protoMessagesList = new ArrayList<>(value.size());
+ for (ByteArray protoMessageBytes : value) {
+ if (protoMessageBytes == null) {
+ protoMessagesList.add(null);
+ } else {
+ protoMessagesList.add(
+ (T) m.toBuilder().mergeFrom(protoMessageBytes.toByteArray()).build());
+ }
+ }
+ return protoMessagesList;
+ } catch (InvalidProtocolBufferException e) {
+ throw SpannerExceptionFactory.asSpannerException(e);
+ }
+ }
+
+ @Override
+ String elementToString(ByteArray element) {
+ return element.toBase64();
+ }
+
+ @Override
+ void appendElement(StringBuilder b, ByteArray element) {
+ b.append(element.toString());
+ }
+ }
+
+ private static class ProtoEnumArrayImpl extends AbstractArrayValue {
+
+ private ProtoEnumArrayImpl(boolean isNull, @Nullable List values, String protoTypeFqn) {
+ super(isNull, Type.protoEnum(protoTypeFqn), values);
+ }
+
+ @Override
+ public List getInt64Array() {
+ return value;
+ }
+
+ @Override
+ public List getProtoEnumArray(
+ Function method) {
+ Preconditions.checkNotNull(
+ method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value.");
+ checkNotNull();
+
+ List protoEnumList = new ArrayList<>();
+ for (Long enumIntValue : value) {
+ if (enumIntValue == null) {
+ protoEnumList.add(null);
+ } else {
+ protoEnumList.add((T) method.apply(enumIntValue.intValue()));
+ }
+ }
+ return protoEnumList;
+ }
+
+ @Override
+ String elementToString(Long element) {
+ return Long.toString(element);
+ }
+
+ @Override
+ void appendElement(StringBuilder b, Long element) {
+ b.append(element);
+ }
+ }
+
private static class DateArrayImpl extends AbstractArrayValue {
private DateArrayImpl(boolean isNull, @Nullable List values) {
@@ -2148,6 +2588,10 @@ private Value getValue(int fieldIndex) {
return Value.date(value.getDate(fieldIndex));
case TIMESTAMP:
return Value.timestamp(value.getTimestamp(fieldIndex));
+ case PROTO:
+ return Value.protoMessage(value.getBytes(fieldIndex), fieldType.getProtoTypeFqn());
+ case ENUM:
+ return Value.protoEnum(value.getLong(fieldIndex), fieldType.getProtoTypeFqn());
case STRUCT:
return Value.struct(value.getStruct(fieldIndex));
case ARRAY:
@@ -2157,6 +2601,7 @@ private Value getValue(int fieldIndex) {
case BOOL:
return Value.boolArray(value.getBooleanList(fieldIndex));
case INT64:
+ case ENUM:
return Value.int64Array(value.getLongList(fieldIndex));
case STRING:
return Value.stringArray(value.getStringList(fieldIndex));
@@ -2165,6 +2610,7 @@ private Value getValue(int fieldIndex) {
case PG_JSONB:
return Value.pgJsonbArray(value.getPgJsonbList(fieldIndex));
case BYTES:
+ case PROTO:
return Value.bytesArray(value.getBytesList(fieldIndex));
case FLOAT64:
return Value.float64Array(value.getDoubleList(fieldIndex));
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 07066470da6..9915e12175a 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
@@ -19,6 +19,10 @@
import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.Timestamp;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumDescriptor;
+import com.google.protobuf.ProtocolMessageEnum;
import java.math.BigDecimal;
import javax.annotation.Nullable;
@@ -97,6 +101,41 @@ public R to(@Nullable String value) {
return handle(Value.string(value));
}
+ /** Binds to {@code Value.protoMessage(value)} */
+ public R to(AbstractMessage m) {
+ return handle(Value.protoMessage(m));
+ }
+
+ /** Binds to {@code Value.protoMessage(value, protoType)} */
+ public R to(@Nullable ByteArray v, String protoTypFqn) {
+ return handle(Value.protoMessage(v, protoTypFqn));
+ }
+
+ /** Binds to {@code Value.protoMessage(value, descriptor)} */
+ public R to(@Nullable ByteArray v, Descriptor descriptor) {
+ return handle(Value.protoMessage(v, descriptor));
+ }
+
+ /** Binds to {@code Value.protoEnum(value)} */
+ public R to(ProtocolMessageEnum value) {
+ return handle(Value.protoEnum(value));
+ }
+
+ /** Binds to {@code Value.protoEnum(value, protoType)} */
+ public R to(@Nullable Long v, String protoTypFqn) {
+ return handle(Value.protoEnum(v, protoTypFqn));
+ }
+
+ /** Binds to {@code Value.protoEnum(value, enumDescriptor)} */
+ public R to(@Nullable Long v, EnumDescriptor enumDescriptor) {
+ return handle(Value.protoEnum(v, enumDescriptor));
+ }
+
+ /** Binds to {@code Value.protoEnum(value, protoType)} */
+ public R to(long v, String protoTypFqn) {
+ return handle(Value.protoEnum(v, protoTypFqn));
+ }
+
/**
* Binds to {@code Value.bytes(value)}. Use {@link #to(Value)} in combination with {@link
* Value#bytesFromBase64(String)} if you already have the value that you want to bind in base64
@@ -218,6 +257,27 @@ public R toTimestampArray(@Nullable Iterable values) {
return handle(Value.timestampArray(values));
}
+ /** Binds to {@code Value.protoMessageArray(values, descriptor)} */
+ public R toProtoMessageArray(@Nullable Iterable values, Descriptor descriptor) {
+ return handle(Value.protoMessageArray(values, descriptor));
+ }
+
+ /** Binds to {@code Value.protoMessageArray(values, protoTypeFq)} */
+ public R toProtoMessageArray(@Nullable Iterable values, String protoTypeFq) {
+ return handle(Value.protoMessageArray(values, protoTypeFq));
+ }
+
+ /** Binds to {@code Value.protoEnumArray(values, descriptor)} */
+ public R toProtoEnumArray(
+ @Nullable Iterable values, EnumDescriptor descriptor) {
+ return handle(Value.protoEnumArray(values, descriptor));
+ }
+
+ /** Binds to {@code Value.protoEnumArray(values, protoTypeFq)} */
+ public R toProtoEnumArray(@Nullable Iterable values, String protoTypeFq) {
+ return handle(Value.protoEnumArray(values, protoTypeFq));
+ }
+
/** Binds to {@code Value.dateArray(values)} */
public R toDateArray(@Nullable Iterable values) {
return handle(Value.dateArray(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 bb2f2fb817a..24389546669 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
@@ -225,6 +225,7 @@ public void funnel(Struct row, PrimitiveSink into) {
funnelValue(type, row.getBoolean(i), into);
break;
case BYTES:
+ case PROTO:
funnelValue(type, row.getBytes(i), into);
break;
case DATE:
@@ -240,6 +241,7 @@ public void funnel(Struct row, PrimitiveSink into) {
funnelValue(type, row.getString(i), into);
break;
case INT64:
+ case ENUM:
funnelValue(type, row.getLong(i), into);
break;
case STRING:
@@ -274,6 +276,7 @@ private void funnelArray(
}
break;
case BYTES:
+ case PROTO:
into.putInt(row.getBytesList(columnIndex).size());
for (ByteArray value : row.getBytesList(columnIndex)) {
funnelValue(Code.BYTES, value, into);
@@ -304,6 +307,7 @@ private void funnelArray(
}
break;
case INT64:
+ case ENUM:
into.putInt(row.getLongList(columnIndex).size());
for (Long value : row.getLongList(columnIndex)) {
funnelValue(Code.INT64, value, into);
@@ -357,6 +361,7 @@ private void funnelValue(Code type, T value, PrimitiveSink into) {
into.putBoolean((Boolean) value);
break;
case BYTES:
+ case PROTO:
ByteArray byteArray = (ByteArray) value;
into.putInt(byteArray.length());
into.putBytes(byteArray.toByteArray());
@@ -374,6 +379,7 @@ private void funnelValue(Code type, T value, PrimitiveSink into) {
into.putUnencodedChars(stringRepresentation);
break;
case INT64:
+ case ENUM:
into.putLong((Long) value);
break;
case PG_NUMERIC:
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 8690e154f4e..dff915e2cce 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
@@ -25,10 +25,13 @@
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Value;
import com.google.common.base.Preconditions;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import com.google.spanner.v1.ResultSetMetadata;
import com.google.spanner.v1.ResultSetStats;
import java.math.BigDecimal;
import java.util.List;
+import java.util.function.Function;
/**
* {@link ResultSet} implementation used by the Spanner connection API to ensure that the query for
@@ -425,6 +428,32 @@ public List getDateList(String columnName) {
return delegate.getDateList(columnName);
}
+ @Override
+ public List getProtoMessageList(int columnIndex, T message) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoMessageList(columnIndex, message);
+ }
+
+ @Override
+ public List getProtoMessageList(String columnName, T message) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoMessageList(columnName, message);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ int columnIndex, Function method) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoEnumList(columnIndex, method);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ String columnName, Function method) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoEnumList(columnName, method);
+ }
+
@Override
public List getStructList(int columnIndex) {
Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
@@ -437,6 +466,32 @@ public List getStructList(String columnName) {
return delegate.getStructList(columnName);
}
+ @Override
+ public T getProtoEnum(
+ int columnIndex, Function method) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoEnum(columnIndex, method);
+ }
+
+ @Override
+ public T getProtoEnum(
+ String columnName, Function method) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoEnum(columnName, method);
+ }
+
+ @Override
+ public T getProtoMessage(int columnIndex, T message) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoMessage(columnIndex, message);
+ }
+
+ @Override
+ public T getProtoMessage(String columnName, T message) {
+ Preconditions.checkState(nextCalledByClient, MISSING_NEXT_CALL);
+ return delegate.getProtoMessage(columnName, message);
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof DirectExecuteResultSet)) {
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 07e755b2b25..7370551a46f 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
@@ -27,10 +27,13 @@
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Value;
import com.google.common.base.Preconditions;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
import com.google.spanner.v1.ResultSetMetadata;
import com.google.spanner.v1.ResultSetStats;
import java.math.BigDecimal;
import java.util.List;
+import java.util.function.Function;
/**
* Forwarding implementation of {@link ResultSet} that forwards all calls to a delegate that can be
@@ -430,6 +433,32 @@ public List getDateList(String columnName) {
return delegate.getDateList(columnName);
}
+ @Override
+ public List getProtoMessageList(int columnIndex, T message) {
+ checkClosed();
+ return delegate.getProtoMessageList(columnIndex, message);
+ }
+
+ @Override
+ public List getProtoMessageList(String columnName, T message) {
+ checkClosed();
+ return delegate.getProtoMessageList(columnName, message);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ int columnIndex, Function method) {
+ checkClosed();
+ return delegate.getProtoEnumList(columnIndex, method);
+ }
+
+ @Override
+ public List getProtoEnumList(
+ String columnName, Function method) {
+ checkClosed();
+ return delegate.getProtoEnumList(columnName, method);
+ }
+
@Override
public List getStructList(int columnIndex) {
checkClosed();
@@ -441,4 +470,30 @@ public List getStructList(String columnName) {
checkClosed();
return delegate.getStructList(columnName);
}
+
+ @Override
+ public T getProtoMessage(int columnIndex, T message) {
+ checkClosed();
+ return delegate.getProtoMessage(columnIndex, message);
+ }
+
+ @Override
+ public T getProtoMessage(String columnName, T message) {
+ checkClosed();
+ return delegate.getProtoMessage(columnName, message);
+ }
+
+ @Override
+ public T getProtoEnum(
+ int columnIndex, Function method) {
+ checkClosed();
+ return delegate.getProtoEnum(columnIndex, method);
+ }
+
+ @Override
+ public T getProtoEnum(
+ String columnName, Function