From 30d37dd80c91b2dffdfee732677607ce028fb8d2 Mon Sep 17 00:00:00 2001 From: Sri Harsha CH <57220027+harshachinta@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:03:24 +0530 Subject: [PATCH] feat: add support for Proto Columns (#2779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Support for Proto Messages & Enums (#2155) * feat: Importing Proto Changes Commit will be reverted, once PROTO changes are available publicly. * feat: Proto Message Implementation * feat: Adding support for enum * feat: Code refactoring Adding default implementation for newly added methods ByteArray compatability changes for Proto Messages * docs: Adding Java docs for all the newly added methods. * test: Sample Proto & Generated classes for unit test * feat: Adding bytes/proto & int64/enum compatability Adding Additional check for ChecksumResultSet * test: Adding unit tests * test: Adding unit tests for ValueBinder.java * feat: refactoring to add support for getValue & other minor changes * feat: Minor refactoring 1. Adding docs and formatting the code. 2. Adding additional methods for enum and message which accepts descriptors. * feat: Adding bytes/message & int64/enum compatability in Value * refactor: Minor refactoring * feat: Adding Proto Array Implementation * test: Implementing unit tests for array of protos and enums * refactor: adding clirr ignores * feat: Adding support for enum as Primary Key * feat: Code Review Changes, minor refactoring and adding docs * feat: Addressing review comments -Modified Docs/Comments -Minor Refactoring * refactor: Using Column instead of column to avoid test failures * feat: Minor refactoring -code review comments -adding function docs * samples: Adding samples for updating & querying Proto messages & enums (#2211) * samples: Adding samples for updating & querying Proto messages & enums * style: linting * style: linting * docs: Adding function and class doc * test: Proto Column Integration tests (#2212) * test: Adding Integration tests for Proto Messages & Enums * test: Adding additional test for Parameterized Queries, Primary Keys & Invalid Wire type errors. * style: Formatting * style: Formatting * test: Updating instance and db name * test: Adding inter compatability check while writing data * Configured jitpack.yml to use OpenJDK 11 (#2218) Co-authored-by: Pavol Juhos * feat: add support for Proto Columns DDL (#2277) * feat: add code changes and tests for Proto columns DDL support * feat: add auto generated code * feat: code changes and tests for Proto columns DDL support * feat: add descriptors file * feat: code refactoring * feat: Integration tests and code refactoring * feat: code refactoring * feat: unit tests and clirr differences * feat: lint changes * feat: code refactor * feat: code refactoring * feat: code refactoring * feat: code refactoring * feat: add java docs to new methods * feat: lint formatting * feat: lint formatting changes * feat: lint formatting * feat: lint formatting * feat: test exception cases * feat: code refactoring * feat: add java docs and refactoring * feat: add java docs * feat: java docs refactor * feat: remove overload method setProtoDescriptors that accepts file path as input to avoid unexpected issues * feat: remove updateDdl method overload to update proto descriptor * teat: update pom file to run tests on cloud-devel region temporarily to validate main branch update * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix: revert host changes in pom.xml file * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat(spanner): revert autogenerated code * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat(spanner): remove samples * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat(spanner): remove clirr * feat(spanner): skip emulator test * feat(spanner): clirr * feat(spanner): fix javadoc * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat(spanner): fix javadoc * feat(spanner): fix javadoc * feat(spanner): fix javadoc * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat(spanner): fix emulator skip * build: ignore all changes in v1 package * feat(spanner): add optimizations to deserialize proto messages * feat(spanner): remove TODO * feat(spanner): remove TODO --------- Co-authored-by: Gaurav Purohit Co-authored-by: Pavol Juhos Co-authored-by: Owl Bot Co-authored-by: Knut Olav Løite --- .../clirr-ignored-differences.xml | 79 ++ google-cloud-spanner/pom.xml | 9 + .../cloud/spanner/AbstractResultSet.java | 138 +- .../cloud/spanner/AbstractStructReader.java | 128 +- .../cloud/spanner/DatabaseAdminClient.java | 55 +- .../spanner/DatabaseAdminClientImpl.java | 19 +- .../google/cloud/spanner/DatabaseInfo.java | 67 +- .../cloud/spanner/ForwardingStructReader.java | 55 + .../java/com/google/cloud/spanner/Key.java | 12 + .../com/google/cloud/spanner/ResultSets.java | 47 + .../java/com/google/cloud/spanner/Struct.java | 30 + .../google/cloud/spanner/StructReader.java | 115 ++ .../java/com/google/cloud/spanner/Type.java | 47 +- .../java/com/google/cloud/spanner/Value.java | 448 +++++- .../com/google/cloud/spanner/ValueBinder.java | 60 + .../spanner/connection/ChecksumResultSet.java | 6 + .../connection/DirectExecuteResultSet.java | 55 + .../ReplaceableForwardingResultSet.java | 55 + .../cloud/spanner/spi/v1/GapicSpannerRpc.java | 33 +- .../cloud/spanner/spi/v1/SpannerRpc.java | 7 +- .../AbstractStructReaderTypesTest.java | 26 + .../spanner/DatabaseAdminClientImplTest.java | 26 +- .../google/cloud/spanner/DatabaseTest.java | 58 + .../cloud/spanner/GrpcResultSetTest.java | 135 ++ .../google/cloud/spanner/MutationTest.java | 28 + .../google/cloud/spanner/ResultSetsTest.java | 84 +- .../com/google/cloud/spanner/SingerProto.java | 1199 +++++++++++++++++ .../com/google/cloud/spanner/TypeTest.java | 64 +- .../google/cloud/spanner/ValueBinderTest.java | 42 +- .../com/google/cloud/spanner/ValueTest.java | 116 ++ .../connection/ChecksumResultSetTest.java | 47 +- .../DirectExecuteResultSetTest.java | 52 +- .../connection/RandomResultSetGenerator.java | 171 ++- .../connection/ReadWriteTransactionTest.java | 74 +- .../ReplaceableForwardingResultSetTest.java | 52 +- .../cloud/spanner/it/ITProtoColumnTest.java | 402 ++++++ .../com/google/cloud/spanner/descriptors.pb | Bin 0 -> 251 bytes .../com/google/cloud/spanner/singer.proto | 21 + 38 files changed, 3945 insertions(+), 117 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java create mode 100644 google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb create mode 100644 google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 8482230632..56e946eb3d 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -192,6 +192,46 @@ com/google/cloud/spanner/StructReader java.util.List getPgJsonbList(java.lang.String) + + 7012 + com/google/cloud/spanner/StructReader + com.google.protobuf.ProtocolMessageEnum getProtoEnum(int, java.util.function.Function) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.protobuf.ProtocolMessageEnum getProtoEnum(java.lang.String, java.util.function.Function) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.protobuf.AbstractMessage getProtoMessage(int, com.google.protobuf.AbstractMessage) + + + 7012 + com/google/cloud/spanner/StructReader + com.google.protobuf.AbstractMessage getProtoMessage(java.lang.String, com.google.protobuf.AbstractMessage) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getProtoEnumList(int, java.util.function.Function) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getProtoEnumList(java.lang.String, java.util.function.Function) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getProtoMessageList(int, com.google.protobuf.AbstractMessage) + + + 7012 + com/google/cloud/spanner/StructReader + java.util.List getProtoMessageList(java.lang.String, com.google.protobuf.AbstractMessage) + 7012 com/google/cloud/spanner/BatchClient @@ -222,6 +262,38 @@ com/google/cloud/spanner/connection/Connection com.google.cloud.spanner.ResultSet analyzeUpdateStatement(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode, com.google.cloud.spanner.Options$UpdateOption[]) + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.spanner.admin.database.v1.GetDatabaseDdlResponse getDatabaseDdlResponse(java.lang.String, java.lang.String) + + + 7012 + com/google/cloud/spanner/DatabaseAdminClient + com.google.api.gax.longrunning.OperationFuture updateDatabaseDdl(com.google.cloud.spanner.Database, java.lang.Iterable, java.lang.String) + + + 7013 + com/google/cloud/spanner/DatabaseInfo$Builder + com.google.cloud.spanner.DatabaseInfo$Builder setProtoDescriptors(byte[]) + + + 7013 + com/google/cloud/spanner/DatabaseInfo$Builder + com.google.cloud.spanner.DatabaseInfo$Builder setProtoDescriptors(java.io.InputStream) + + + 7013 + com/google/cloud/spanner/DatabaseInfo$Builder + com.google.cloud.spanner.DatabaseInfo$Builder setProtoDescriptors(java.lang.String) + + + 7006 + com/google/cloud/spanner/spi/v1/GapicSpannerRpc + java.util.List getDatabaseDdl(java.lang.String) + java.util.List + com.google.spanner.admin.database.v1.GetDatabaseDdlResponse + 7004 com/google/cloud/spanner/spi/v1/GapicSpannerRpc @@ -277,6 +349,13 @@ com.google.spanner.v1.ResultSet executeQuery(com.google.spanner.v1.ExecuteSqlRequest, java.util.Map) com.google.cloud.spanner.spi.v1.SpannerRpc$StreamingCall + + 7006 + com/google/cloud/spanner/spi/v1/SpannerRpc + java.util.List getDatabaseDdl(java.lang.String) + java.util.List + com.google.spanner.admin.database.v1.GetDatabaseDdlResponse + 7004 com/google/cloud/spanner/spi/v1/SpannerRpc diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 24abc0d219..999f49b77f 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -134,6 +134,15 @@ org.codehaus.mojo flatten-maven-plugin + + + org.codehaus.mojo + clirr-maven-plugin + + + com/google/cloud/spanner/spi/v1/** + + 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 b0d5ab2bba..c18e64165b 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 @@ -39,10 +39,13 @@ import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; +import com.google.common.io.CharSource; import com.google.common.util.concurrent.Uninterruptibles; +import com.google.protobuf.AbstractMessage; import com.google.protobuf.ByteString; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; +import com.google.protobuf.ProtocolMessageEnum; import com.google.protobuf.Value.KindCase; import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.ResultSetMetadata; @@ -58,6 +61,7 @@ import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; import java.util.AbstractList; import java.util.ArrayList; import java.util.Base64; @@ -73,6 +77,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -477,6 +482,14 @@ private Object writeReplace() { case JSON: builder.set(fieldName).to(Value.json((String) value)); break; + case PROTO: + builder + .set(fieldName) + .to(Value.protoMessage((ByteArray) value, fieldType.getProtoTypeFqn())); + break; + case ENUM: + builder.set(fieldName).to(Value.protoEnum((Long) value, fieldType.getProtoTypeFqn())); + break; case PG_JSONB: builder.set(fieldName).to(Value.pgJsonb((String) value)); break; @@ -500,6 +513,7 @@ private Object writeReplace() { builder.set(fieldName).toBoolArray((Iterable) value); break; case INT64: + case ENUM: builder.set(fieldName).toInt64Array((Iterable) value); break; case FLOAT64: @@ -521,6 +535,7 @@ private Object writeReplace() { builder.set(fieldName).toPgJsonbArray((Iterable) value); break; case BYTES: + case PROTO: builder .set(fieldName) .toBytesArrayFromBase64( @@ -596,6 +611,7 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot checkType(fieldType, proto, KindCase.BOOL_VALUE); return proto.getBoolValue(); case INT64: + case ENUM: checkType(fieldType, proto, KindCase.STRING_VALUE); return Long.parseLong(proto.getStringValue()); case FLOAT64: @@ -610,6 +626,7 @@ private static Object decodeValue(Type fieldType, com.google.protobuf.Value prot checkType(fieldType, proto, KindCase.STRING_VALUE); return proto.getStringValue(); case BYTES: + case PROTO: checkType(fieldType, proto, KindCase.STRING_VALUE); return new LazyByteArray(proto.getStringValue()); case TIMESTAMP: @@ -649,7 +666,8 @@ private static Struct decodeStructValue(Type structType, ListValue structValue) static Object decodeArrayValue(Type elementType, ListValue listValue) { switch (elementType.getCode()) { case INT64: - // For int64/float64 types, use custom containers. These avoid wrapper object + case ENUM: + // For int64/float64/enum types, use custom containers. These avoid wrapper object // creation for non-null arrays. return new Int64Array(listValue); case FLOAT64: @@ -664,6 +682,7 @@ static Object decodeArrayValue(Type elementType, ListValue listValue) { case TIMESTAMP: case DATE: case STRUCT: + case PROTO: return Lists.transform( listValue.getValuesList(), input -> decodeValue(elementType, input)); default: @@ -699,6 +718,35 @@ public boolean isNull(int columnIndex) { return rowData.get(columnIndex) == null; } + @Override + protected T getProtoMessageInternal(int columnIndex, T message) { + Preconditions.checkNotNull( + message, + "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); + try { + return (T) + message + .toBuilder() + .mergeFrom( + Base64.getDecoder() + .wrap( + CharSource.wrap(((LazyByteArray) rowData.get(columnIndex)).base64String) + .asByteSource(StandardCharsets.UTF_8) + .openStream())) + .build(); + } catch (IOException ioException) { + throw SpannerExceptionFactory.asSpannerException(ioException); + } + } + + @Override + protected T getProtoEnumInternal( + int columnIndex, Function method) { + Preconditions.checkNotNull( + method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); + return (T) method.apply((int) getLongInternal(columnIndex)); + } + @Override protected boolean getBooleanInternal(int columnIndex) { return (Boolean) rowData.get(columnIndex); @@ -768,6 +816,8 @@ protected Value getValueInternal(int columnIndex) { return Value.bool(isNull ? null : getBooleanInternal(columnIndex)); case INT64: return Value.int64(isNull ? null : getLongInternal(columnIndex)); + case ENUM: + return Value.protoEnum(getLongInternal(columnIndex), columnType.getProtoTypeFqn()); case NUMERIC: return Value.numeric(isNull ? null : getBigDecimalInternal(columnIndex)); case PG_NUMERIC: @@ -782,6 +832,8 @@ protected Value getValueInternal(int columnIndex) { return Value.pgJsonb(isNull ? null : getPgJsonbInternal(columnIndex)); case BYTES: return Value.internalBytes(isNull ? null : getLazyBytesInternal(columnIndex)); + case PROTO: + return Value.protoMessage(getBytesInternal(columnIndex), columnType.getProtoTypeFqn()); case TIMESTAMP: return Value.timestamp(isNull ? null : getTimestampInternal(columnIndex)); case DATE: @@ -812,6 +864,12 @@ protected Value getValueInternal(int columnIndex) { return Value.pgJsonbArray(isNull ? null : getPgJsonbListInternal(columnIndex)); case BYTES: return Value.bytesArray(isNull ? null : getBytesListInternal(columnIndex)); + case PROTO: + return Value.protoMessageArray( + isNull ? null : getBytesListInternal(columnIndex), elementType.getProtoTypeFqn()); + case ENUM: + return Value.protoEnumArray( + isNull ? null : getLongListInternal(columnIndex), elementType.getProtoTypeFqn()); case TIMESTAMP: return Value.timestampArray(isNull ? null : getTimestampListInternal(columnIndex)); case DATE: @@ -891,6 +949,61 @@ protected List getJsonListInternal(int columnIndex) { return Collections.unmodifiableList((List) rowData.get(columnIndex)); } + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getProtoMessageListInternal( + int columnIndex, T message) { + Preconditions.checkNotNull( + message, + "Proto message may not be null. Use MyProtoClass.getDefaultInstance() as a parameter value."); + + List bytesArray = (List) rowData.get(columnIndex); + + try { + List protoMessagesList = new ArrayList<>(bytesArray.size()); + for (LazyByteArray protoMessageBytes : bytesArray) { + if (protoMessageBytes == null) { + protoMessagesList.add(null); + } else { + protoMessagesList.add( + (T) + message + .toBuilder() + .mergeFrom( + Base64.getDecoder() + .wrap( + CharSource.wrap(protoMessageBytes.base64String) + .asByteSource(StandardCharsets.UTF_8) + .openStream())) + .build()); + } + } + return protoMessagesList; + } catch (IOException ioException) { + throw SpannerExceptionFactory.asSpannerException(ioException); + } + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getProtoEnumListInternal( + int columnIndex, Function method) { + Preconditions.checkNotNull( + method, "Method may not be null. Use 'MyProtoEnum::forNumber' as a parameter value."); + + List enumIntArray = (List) rowData.get(columnIndex); + List protoEnumList = new ArrayList<>(enumIntArray.size()); + for (Long enumIntValue : enumIntArray) { + if (enumIntValue == null) { + protoEnumList.add(null); + } else { + protoEnumList.add((T) method.apply(enumIntValue.intValue())); + } + } + + return protoEnumList; + } + @Override @SuppressWarnings("unchecked") // We know ARRAY produces a List. protected List getPgJsonbListInternal(int columnIndex) { @@ -1489,6 +1602,17 @@ protected String getStringInternal(int columnIndex) { return currRow().getStringInternal(columnIndex); } + @Override + protected T getProtoMessageInternal(int columnIndex, T message) { + return currRow().getProtoMessageInternal(columnIndex, message); + } + + @Override + protected T getProtoEnumInternal( + int columnIndex, Function method) { + return currRow().getProtoEnumInternal(columnIndex, method); + } + @Override protected String getJsonInternal(int columnIndex) { return currRow().getJsonInternal(columnIndex); @@ -1574,6 +1698,18 @@ protected List getBytesListInternal(int columnIndex) { return currRow().getBytesListInternal(columnIndex); } + @Override + protected List getProtoMessageListInternal( + int columnIndex, T message) { + return currRow().getProtoMessageListInternal(columnIndex, message); + } + + @Override + protected List getProtoEnumListInternal( + int columnIndex, Function method) { + return currRow().getProtoEnumListInternal(columnIndex, method); + } + @Override protected List getTimestampListInternal(int columnIndex) { return currRow().getTimestampListInternal(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 c868678f10..53c49a5a54 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 @@ -21,9 +21,14 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Type.Code; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ProtocolMessageEnum; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.function.Function; /** * Base class for assisting {@link StructReader} implementations. @@ -58,6 +63,25 @@ protected String getPgJsonbInternal(int columnIndex) { protected abstract Date getDateInternal(int columnIndex); + protected T getProtoMessageInternal(int columnIndex, T message) { + throw new UnsupportedOperationException("Not implemented"); + } + + protected T getProtoEnumInternal( + int columnIndex, Function method) { + throw new UnsupportedOperationException("Not implemented"); + } + + protected List getProtoMessageListInternal( + int columnIndex, T message) { + throw new UnsupportedOperationException("Not implemented"); + } + + protected List getProtoEnumListInternal( + int columnIndex, Function method) { + throw new UnsupportedOperationException("Not implemented"); + } + protected Value getValueInternal(int columnIndex) { throw new UnsupportedOperationException("method should be overwritten"); } @@ -129,14 +153,14 @@ public boolean getBoolean(String columnName) { @Override public long getLong(int columnIndex) { - checkNonNullOfType(columnIndex, Type.int64(), columnIndex); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnIndex); return getLongInternal(columnIndex); } @Override public long getLong(String columnName) { int columnIndex = getColumnIndex(columnName); - checkNonNullOfType(columnIndex, Type.int64(), columnName); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnName); return getLongInternal(columnIndex); } @@ -212,14 +236,14 @@ public String getPgJsonb(String columnName) { @Override public ByteArray getBytes(int columnIndex) { - checkNonNullOfType(columnIndex, Type.bytes(), columnIndex); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnIndex); return getBytesInternal(columnIndex); } @Override public ByteArray getBytes(String columnName) { int columnIndex = getColumnIndex(columnName); - checkNonNullOfType(columnIndex, Type.bytes(), columnName); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnName); return getBytesInternal(columnIndex); } @@ -249,6 +273,34 @@ public Date getDate(String columnName) { return getDateInternal(columnIndex); } + @Override + public T getProtoEnum( + int columnIndex, Function method) { + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnIndex); + return getProtoEnumInternal(columnIndex, method); + } + + @Override + public T getProtoEnum( + String columnName, Function method) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnName); + return getProtoEnumInternal(columnIndex, method); + } + + @Override + public T getProtoMessage(int columnIndex, T message) { + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnIndex); + return getProtoMessageInternal(columnIndex, message); + } + + @Override + public T getProtoMessage(String columnName, T message) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfCodes(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnName); + return getProtoMessageInternal(columnIndex, message); + } + @Override public Value getValue(int columnIndex) { return getValueInternal(columnIndex); @@ -301,14 +353,16 @@ public long[] getLongArray(String columnName) { @Override public List getLongList(int columnIndex) { - checkNonNullOfType(columnIndex, Type.array(Type.int64()), columnIndex); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnIndex); + checkArrayElementType(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnIndex); return getLongListInternal(columnIndex); } @Override public List getLongList(String columnName) { int columnIndex = getColumnIndex(columnName); - checkNonNullOfType(columnIndex, Type.array(Type.int64()), columnName); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnName); + checkArrayElementType(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnName); return getLongListInternal(columnIndex); } @@ -400,17 +454,51 @@ public List getPgJsonbList(String columnName) { @Override public List getBytesList(int columnIndex) { - checkNonNullOfType(columnIndex, Type.array(Type.bytes()), columnIndex); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnIndex); + checkArrayElementType(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnIndex); return getBytesListInternal(columnIndex); } @Override public List getBytesList(String columnName) { int columnIndex = getColumnIndex(columnName); - checkNonNullOfType(columnIndex, Type.array(Type.bytes()), columnName); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnName); + checkArrayElementType(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnName); return getBytesListInternal(columnIndex); } + @Override + public List getProtoMessageList(int columnIndex, T message) { + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnIndex); + checkArrayElementType(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnIndex); + return getProtoMessageListInternal(columnIndex, message); + } + + @Override + public List getProtoMessageList(String columnName, T message) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnName); + checkArrayElementType(columnIndex, Arrays.asList(Code.PROTO, Code.BYTES), columnName); + return getProtoMessageListInternal(columnIndex, message); + } + + @Override + public List getProtoEnumList( + int columnIndex, Function method) { + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnIndex); + checkArrayElementType(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnIndex); + return getProtoEnumListInternal(columnIndex, method); + } + + @Override + public List getProtoEnumList( + String columnName, Function method) { + int columnIndex = getColumnIndex(columnName); + checkNonNullOfCodes(columnIndex, Collections.singletonList(Code.ARRAY), columnName); + checkArrayElementType(columnIndex, Arrays.asList(Code.ENUM, Code.INT64), columnName); + return getProtoEnumListInternal(columnIndex, method); + } + @Override public List getTimestampList(int columnIndex) { checkNonNullOfType(columnIndex, Type.array(Type.timestamp()), columnIndex); @@ -476,6 +564,30 @@ private void checkNonNullOfType(int columnIndex, Type expectedType, Object colum checkNonNull(columnIndex, columnNameForError); } + /** Checks if the value at {@code columnIndex} is one of {@code expectedCode} */ + private void checkNonNullOfCodes( + int columnIndex, List 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 8372bb61fd..f961a13845 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 8a5d0d613a..f53ae3bb0d 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 d231ef34e3..3f1a0f81eb 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 2a85006fa9..97c39c00a8 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 15d4e995bf..3467052605 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 fa054ba0cd..d55d4091b9 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 48c989d145..40c30148d0 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 ad085ca2dc..fd8cb77f39 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 37262cc6b3..348db5d04a 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 c30847d6fe..7cc7b67578 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 07066470da..9915e12175 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 bb2f2fb817..2438954666 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 8690e154f4..dff915e2cc 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 07e755b2b2..7370551a46 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 method) { + checkClosed(); + return delegate.getProtoEnum(columnName, method); + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index d75b6636a5..a65e533338 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -118,6 +118,7 @@ import com.google.spanner.admin.database.v1.DropDatabaseRequest; import com.google.spanner.admin.database.v1.GetBackupRequest; import com.google.spanner.admin.database.v1.GetDatabaseDdlRequest; +import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse; import com.google.spanner.admin.database.v1.GetDatabaseRequest; import com.google.spanner.admin.database.v1.ListBackupOperationsRequest; import com.google.spanner.admin.database.v1.ListBackupOperationsResponse; @@ -1156,8 +1157,10 @@ public OperationFuture createDatabase( if (databaseInfo.getDialect() != null) { requestBuilder.setDatabaseDialect(databaseInfo.getDialect().toProto()); } + if (databaseInfo.getProtoDescriptors() != null) { + requestBuilder.setProtoDescriptors(databaseInfo.getProtoDescriptors()); + } final CreateDatabaseRequest request = requestBuilder.build(); - OperationFutureCallable callable = new OperationFutureCallable<>( databaseAdminStub.createDatabaseOperationCallable(), @@ -1212,19 +1215,27 @@ public OperationFuture createDatabase( */ @Override public OperationFuture updateDatabaseDdl( - final String databaseName, + com.google.cloud.spanner.Database databaseInfo, final Iterable updateDatabaseStatements, @Nullable final String updateId) throws SpannerException { acquireAdministrativeRequestsRateLimiter(); - final UpdateDatabaseDdlRequest request = + Preconditions.checkNotNull(databaseInfo.getId()); + UpdateDatabaseDdlRequest.Builder requestBuilder = UpdateDatabaseDdlRequest.newBuilder() - .setDatabase(databaseName) + .setDatabase(databaseInfo.getId().getName()) .addAllStatements(updateDatabaseStatements) - .setOperationId(MoreObjects.firstNonNull(updateId, "")) - .build(); + .setOperationId(MoreObjects.firstNonNull(updateId, "")); + if (databaseInfo.getProtoDescriptors() != null) { + requestBuilder.setProtoDescriptors(databaseInfo.getProtoDescriptors()); + } + final UpdateDatabaseDdlRequest request = requestBuilder.build(); final GrpcCallContext context = - newCallContext(null, databaseName, request, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod()); + newCallContext( + null, + databaseInfo.getId().getName(), + request, + DatabaseAdminGrpc.getUpdateDatabaseDdlMethod()); final OperationCallable callable = databaseAdminStub.updateDatabaseDdlOperationCallable(); @@ -1246,7 +1257,7 @@ public OperationFuture updateDatabaseDdl( if (t instanceof AlreadyExistsException) { String operationName = OPERATION_NAME_TEMPLATE.instantiate( - "database", databaseName, "operation", updateId); + "database", databaseInfo.getId().getName(), "operation", updateId); return callable.resumeFutureCall(operationName, context); } } @@ -1293,7 +1304,7 @@ public OperationFuture updateDatabase( } @Override - public List getDatabaseDdl(String databaseName) throws SpannerException { + public GetDatabaseDdlResponse getDatabaseDdl(String databaseName) throws SpannerException { acquireAdministrativeRequestsRateLimiter(); final GetDatabaseDdlRequest request = GetDatabaseDdlRequest.newBuilder().setDatabase(databaseName).build(); @@ -1301,9 +1312,7 @@ public List getDatabaseDdl(String databaseName) throws SpannerException final GrpcCallContext context = newCallContext(null, databaseName, request, DatabaseAdminGrpc.getGetDatabaseDdlMethod()); return runWithRetryOnAdministrativeRequestsExceeded( - () -> - get(databaseAdminStub.getDatabaseDdlCallable().futureCall(request, context)) - .getStatementsList()); + () -> get(databaseAdminStub.getDatabaseDdlCallable().futureCall(request, context))); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 39f798e011..89659e4741 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -43,6 +43,7 @@ import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.Database; import com.google.spanner.admin.database.v1.DatabaseRole; +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; @@ -233,7 +234,9 @@ OperationFuture createDatabase( throws SpannerException; OperationFuture updateDatabaseDdl( - String databaseName, Iterable updateDatabaseStatements, @Nullable String updateId) + com.google.cloud.spanner.Database database, + Iterable updateDatabaseStatements, + @Nullable String updateId) throws SpannerException; void dropDatabase(String databaseName) throws SpannerException; @@ -253,7 +256,7 @@ OperationFuture updateDatabaseDdl( OperationFuture updateDatabase( Database database, FieldMask fieldMask) throws SpannerException; - List getDatabaseDdl(String databaseName) throws SpannerException; + GetDatabaseDdlResponse getDatabaseDdl(String databaseName) throws SpannerException; /** Lists the backups in the specified instance. */ Paginated listBackups( String instanceName, int pageSize, @Nullable String filter, @Nullable String pageToken) 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 1b6280a636..4fc3c67ceb 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 @@ -27,6 +27,8 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.common.base.Throwables; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ProtocolMessageEnum; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; @@ -34,6 +36,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.function.Function; import javax.annotation.Nullable; import org.junit.Before; import org.junit.Test; @@ -95,6 +98,17 @@ protected Date getDateInternal(int columnIndex) { return null; } + @Override + protected T getProtoMessageInternal(int columnIndex, T message) { + return null; + } + + @Override + protected T getProtoEnumInternal( + int columnIndex, Function method) { + return null; + } + @Override protected Value getValueInternal(int columnIndex) { return null; @@ -160,6 +174,18 @@ protected List getTimestampListInternal(int columnIndex) { return null; } + @Override + protected List getProtoMessageListInternal( + int columnIndex, T message) { + return null; + } + + @Override + protected List getProtoEnumListInternal( + int columnIndex, Function method) { + return null; + } + @Override protected List getDateListInternal(int columnIndex) { return null; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java index ace26977bd..8715d4e810 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java @@ -54,6 +54,7 @@ import com.google.spanner.admin.database.v1.DatabaseDialect; import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.EncryptionInfo; +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; @@ -227,7 +228,9 @@ public void updateDatabaseDdl() throws Exception { OperationFuture rawOperationFuture = OperationFutureUtil.immediateOperationFuture( opName, Empty.getDefaultInstance(), UpdateDatabaseDdlMetadata.getDefaultInstance()); - when(rpc.updateDatabaseDdl(DB_NAME, ddl, opId)).thenReturn(rawOperationFuture); + when(rpc.updateDatabaseDdl( + client.newDatabaseBuilder(DatabaseId.of(DB_NAME)).build(), ddl, opId)) + .thenReturn(rawOperationFuture); OperationFuture op = client.updateDatabaseDdl(INSTANCE_ID, DB_ID, ddl, opId); assertThat(op.isDone()).isTrue(); @@ -245,7 +248,9 @@ public void updateDatabaseDdlOpAlreadyExists() throws Exception { UpdateDatabaseDdlMetadata.getDefaultInstance()); String newOpId = "newop"; - when(rpc.updateDatabaseDdl(DB_NAME, ddl, newOpId)).thenReturn(originalOp); + when(rpc.updateDatabaseDdl( + client.newDatabaseBuilder(DatabaseId.of(DB_NAME)).build(), ddl, newOpId)) + .thenReturn(originalOp); OperationFuture op = client.updateDatabaseDdl(INSTANCE_ID, DB_ID, ddl, newOpId); assertThat(op.getName()).isEqualTo(originalOpName); @@ -278,10 +283,25 @@ public void dropDatabase() { @Test public void getDatabaseDdl() { List ddl = ImmutableList.of("CREATE TABLE mytable()"); - when(rpc.getDatabaseDdl(DB_NAME)).thenReturn(ddl); + when(rpc.getDatabaseDdl(DB_NAME)) + .thenReturn(GetDatabaseDdlResponse.newBuilder().addAllStatements(ddl).build()); assertThat(client.getDatabaseDdl(INSTANCE_ID, DB_ID)).isEqualTo(ddl); } + @Test + public void getDatabaseDdlResponse() { + List ddl = ImmutableList.of("CREATE TABLE mytable()"); + when(rpc.getDatabaseDdl(DB_NAME)) + .thenReturn( + GetDatabaseDdlResponse.newBuilder() + .addAllStatements(ddl) + .setProtoDescriptors(ByteString.EMPTY) + .build()); + GetDatabaseDdlResponse response = client.getDatabaseDdlResponse(INSTANCE_ID, DB_ID); + assertThat(response.getStatementsList()).isEqualTo(ddl); + assertThat(response.getProtoDescriptors()).isEqualTo(ByteString.EMPTY); + } + @Test public void listDatabases() { String pageToken = "token"; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java index 78c04ae61b..f49ba026d4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java @@ -19,6 +19,7 @@ import static com.google.cloud.spanner.DatabaseInfo.State.CREATING; 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; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -30,10 +31,14 @@ import com.google.cloud.Timestamp; import com.google.cloud.spanner.DatabaseInfo.State; import com.google.cloud.spanner.encryption.EncryptionConfigs; +import com.google.common.io.ByteStreams; +import com.google.protobuf.ByteString; import com.google.rpc.Code; import com.google.rpc.Status; import com.google.spanner.admin.database.v1.DatabaseDialect; import com.google.spanner.admin.database.v1.EncryptionInfo; +import java.io.IOException; +import java.io.InputStream; import java.util.Collections; import java.util.List; import org.junit.Before; @@ -66,6 +71,10 @@ public class DatabaseTest { .build()); private static final String DEFAULT_LEADER = "default-leader"; private static final DatabaseDialect DEFAULT_DIALECT = DatabaseDialect.GOOGLE_STANDARD_SQL; + private static ByteString protoDescriptors; + private static byte[] protoDescriptorsByteArray; + private static final String PROTO_DESCRIPTORS_RESOURCE_PATH = + "com/google/cloud/spanner/descriptors.pb"; private static final boolean DROP_PROTECTION_ENABLED = true; @@ -83,6 +92,15 @@ public void setUp() { .thenAnswer( invocation -> new Database.Builder(dbClient, (DatabaseId) invocation.getArguments()[0])); + try { + InputStream protoDescriptorsInputStream = + getClass().getClassLoader().getResourceAsStream(PROTO_DESCRIPTORS_RESOURCE_PATH); + assertNotNull(protoDescriptorsInputStream); + protoDescriptorsByteArray = ByteStreams.toByteArray(protoDescriptorsInputStream); + protoDescriptors = ByteString.copyFrom(protoDescriptorsByteArray); + } catch (IOException e) { + e.printStackTrace(); + } } @Test @@ -198,6 +216,46 @@ public void testBuildWithDatabaseDialect() { assertEquals(Dialect.GOOGLE_STANDARD_SQL, database.getDialect()); } + @Test + public void testBuildWithProtoDescriptorsFromInputStream() throws IOException { + InputStream in = + getClass().getClassLoader().getResourceAsStream(PROTO_DESCRIPTORS_RESOURCE_PATH); + assertNotNull(in); + final Database database = + dbClient + .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database")) + .setProtoDescriptors(in) + .build(); + + assertEquals(protoDescriptors, database.getProtoDescriptors()); + } + + @Test + public void testBuildWithProtoDescriptorsFromByteArray() { + final Database database = + dbClient + .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database")) + .setProtoDescriptors(protoDescriptorsByteArray) + .build(); + + assertEquals(protoDescriptors, database.getProtoDescriptors()); + } + + @Test + public void testBuildWithProtoDescriptorsThrowsException() throws IOException { + InputStream in = + getClass().getClassLoader().getResourceAsStream(PROTO_DESCRIPTORS_RESOURCE_PATH); + in.close(); + // case1: Test one of the IOException case, where InputStream is closed before read + assertThrows( + IOException.class, + () -> + dbClient + .newDatabaseBuilder(DatabaseId.of("my-project", "my-instance", "my-database")) + .setProtoDescriptors(in) + .build()); + } + @Test public void getIAMPolicy() { Database database = 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 82300a9309..914ce391f4 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 @@ -27,6 +27,8 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.base.Function; import com.google.common.base.Strings; @@ -765,6 +767,68 @@ public void getPgJsonb() { assertEquals("[]", resultSet.getPgJsonb(0)); } + @Test + public void getProtoMessage() { + SingerInfo singerInfo1 = + SingerInfo.newBuilder() + .setSingerId(111) + .setNationality("COUNTRY1") + .setGenre(Genre.FOLK) + .build(); + SingerInfo singerInfo2 = SingerInfo.newBuilder().setSingerId(222).setGenre(Genre.JAZZ).build(); + String singerInfoFullName = SingerInfo.getDescriptor().getFullName(); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata(Type.struct(Type.StructField.of("f", Type.proto(singerInfoFullName))))) + .addValues(Value.protoMessage(singerInfo1).toProto()) + .addValues( + Value.protoMessage( + ByteArray.copyFrom(singerInfo2.toByteArray()), singerInfoFullName) + .toProto()) + .addValues(Value.protoMessage(null, SingerInfo.getDescriptor().getFullName()).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals(singerInfo1, resultSet.getProtoMessage(0, SingerInfo.getDefaultInstance())); + assertTrue(resultSet.next()); + assertEquals(singerInfo2, resultSet.getProtoMessage(0, SingerInfo.getDefaultInstance())); + assertTrue(resultSet.next()); + assertThrows( + NullPointerException.class, + () -> { + resultSet.getProtoMessage(0, SingerInfo.getDefaultInstance()); + }); + } + + @Test + public void getProtoEnum() { + String genreFullyQualifiedName = Genre.getDescriptor().getFullName(); + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata( + Type.struct(Type.StructField.of("f", Type.protoEnum(genreFullyQualifiedName))))) + .addValues(Value.protoEnum(Genre.FOLK).toProto()) + .addValues(Value.protoEnum(Genre.JAZZ.getNumber(), genreFullyQualifiedName).toProto()) + .addValues(Value.protoEnum(null, genreFullyQualifiedName).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals(Genre.FOLK, resultSet.getProtoEnum(0, Genre::forNumber)); + assertTrue(resultSet.next()); + assertEquals(Genre.JAZZ, resultSet.getProtoEnum(0, Genre::forNumber)); + assertTrue(resultSet.next()); + assertThrows( + NullPointerException.class, + () -> { + resultSet.getProtoEnum(0, Genre::forNumber); + }); + } + @Test public void getBooleanArray() { boolean[] boolArray = {true, true, false}; @@ -906,4 +970,75 @@ public void getPgJsonbList() { assertTrue(resultSet.next()); assertEquals(jsonList, resultSet.getPgJsonbList(0)); } + + @Test + public void getProtoMessageList() { + SingerInfo singerInfo1 = + SingerInfo.newBuilder() + .setSingerId(111) + .setNationality("COUNTRY1") + .setGenre(Genre.FOLK) + .build(); + SingerInfo singerInfo2 = SingerInfo.newBuilder().setSingerId(222).setGenre(Genre.JAZZ).build(); + String singerInfoFullName = SingerInfo.getDescriptor().getFullName(); + + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata( + Type.struct( + Type.StructField.of("f", Type.array(Type.proto(singerInfoFullName)))))) + .addValues( + Value.protoMessageArray( + Arrays.asList(singerInfo1, singerInfo2), SingerInfo.getDescriptor()) + .toProto()) + .addValues( + Value.protoMessageArray( + Arrays.asList(singerInfo2, null, singerInfo1), SingerInfo.getDescriptor()) + .toProto()) + .addValues(Value.protoMessageArray(null, SingerInfo.getDescriptor()).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals( + Arrays.asList(singerInfo1, singerInfo2), + resultSet.getProtoMessageList(0, SingerInfo.getDefaultInstance())); + assertTrue(resultSet.next()); + assertEquals( + Arrays.asList(singerInfo2, null, singerInfo1), + resultSet.getProtoMessageList(0, SingerInfo.getDefaultInstance())); + assertTrue(resultSet.next()); + assertThrows( + NullPointerException.class, + () -> { + resultSet.getProtoMessageList(0, SingerInfo.getDefaultInstance()); + }); + } + + @Test + public void getProtoEnumList() { + String genreFullyQualifiedName = Genre.getDescriptor().getFullName(); + consumer.onPartialResultSet( + PartialResultSet.newBuilder() + .setMetadata( + makeMetadata( + Type.struct(Type.StructField.of("f", Type.protoEnum(genreFullyQualifiedName))))) + .addValues(Value.protoEnum(Genre.FOLK).toProto()) + .addValues(Value.protoEnum(Genre.JAZZ.getNumber(), genreFullyQualifiedName).toProto()) + .addValues(Value.protoEnum(null, genreFullyQualifiedName).toProto()) + .build()); + consumer.onCompleted(); + + assertTrue(resultSet.next()); + assertEquals(Genre.FOLK, resultSet.getProtoEnum(0, Genre::forNumber)); + assertTrue(resultSet.next()); + assertEquals(Genre.JAZZ, resultSet.getProtoEnum(0, Genre::forNumber)); + assertTrue(resultSet.next()); + assertThrows( + NullPointerException.class, + () -> { + resultSet.getProtoEnum(0, Genre::forNumber); + }); + } } 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 fe2b7aec94..f38b5e47b8 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 @@ -23,6 +23,8 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; @@ -545,6 +547,14 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .to(Value.json("{\"key\": \"value\"}}")) .set("jsonNull") .to(Value.json(null)) + .set("protoMessage") + .to(SingerInfo.newBuilder().setSingerId(232).setGenre(Genre.POP).build()) + .set("protoMessageNull") + .to(Value.protoMessage(null, SingerInfo.getDescriptor().getFullName())) + .set("protoEnum") + .to(Genre.JAZZ) + .set("protoEnumNull") + .to(Value.protoEnum(null, SingerInfo.getDescriptor().getFullName())) .set("pgJsonb") .to(Value.pgJsonb("{\"key\": \"value\"}}")) .set("pgJsonbNull") @@ -603,6 +613,24 @@ private Mutation.WriteBuilder appendAllTypes(Mutation.WriteBuilder builder) { .toJsonArray(null) .set("jsonArrValue") .to(Value.jsonArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}"))) + .set("protoMessageArr") + .toProtoMessageArray( + ImmutableList.of(SingerInfo.newBuilder().setSingerId(232).setGenre(Genre.POP).build()), + SingerInfo.getDescriptor()) + .set("protoMessageArrNull") + .toProtoMessageArray(null, SingerInfo.getDescriptor()) + .set("protoMessageArrValue") + .to( + Value.protoMessageArray( + ImmutableList.of( + SingerInfo.newBuilder().setSingerId(232).setGenre(Genre.POP).build()), + SingerInfo.getDescriptor())) + .set("protoEnumArr") + .toProtoEnumArray(ImmutableList.of(Genre.JAZZ), Genre.getDescriptor()) + .set("protoEnumArrNull") + .toProtoEnumArray(null, Genre.getDescriptor()) + .set("protoEnumArrValue") + .to(Value.protoEnumArray(ImmutableList.of(Genre.JAZZ), Genre.getDescriptor())) .set("pgJsonbArr") .toPgJsonbArray(ImmutableList.of("{\"key\": \"value1\"}}", "{\"key\": \"value2\"}")) .set("pgJsonbArrNull") 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 87be602808..8e1f257594 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 @@ -28,9 +28,13 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ProtocolMessageEnum; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; @@ -52,6 +56,13 @@ public void resultSetIteration() { BigDecimal bigDecimalVal = BigDecimal.valueOf(123, 2); String stringVal = "stringVal"; String jsonVal = "{\"color\":\"red\",\"value\":\"#f00\"}"; + SingerInfo protoMessageVal = + SingerInfo.newBuilder() + .setSingerId(111) + .setNationality("COUNTRY1") + .setGenre(Genre.FOLK) + .build(); + ProtocolMessageEnum protoEnumVal = Genre.ROCK; String byteVal = "101"; long usecs = 32343; int year = 2018; @@ -80,6 +91,10 @@ public void resultSetIteration() { }; String[] stringArray = {"abc", "def", "ghi"}; String[] jsonArray = {"{}", "{\"color\":\"red\",\"value\":\"#f00\"}", "[]"}; + AbstractMessage[] protoMessageArray = { + protoMessageVal, SingerInfo.newBuilder().setSingerId(1).build() + }; + ProtocolMessageEnum[] protoEnumArray = {protoEnumVal, Genre.JAZZ}; Type type = Type.struct( @@ -94,6 +109,10 @@ public void resultSetIteration() { Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), + Type.StructField.of( + "protoMessage", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + Type.StructField.of( + "protoEnum", Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName())), Type.StructField.of("boolArray", Type.array(Type.bool())), Type.StructField.of("longArray", Type.array(Type.int64())), Type.StructField.of("doubleArray", Type.array(Type.float64())), @@ -103,7 +122,12 @@ public void resultSetIteration() { 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()))); + Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), + Type.StructField.of( + "protoMessageArray", + Type.array(Type.proto(SingerInfo.getDescriptor().getFullName()))), + Type.StructField.of( + "protoEnumArray", Type.array(Type.protoEnum(Genre.getDescriptor().getFullName())))); Struct struct1 = Struct.newBuilder() .set("f1") @@ -128,6 +152,10 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("protoMessage") + .to(protoMessageVal) + .set("protoEnum") + .to(protoEnumVal) .set("boolArray") .to(Value.boolArray(boolArray)) .set("longArray") @@ -148,6 +176,14 @@ public void resultSetIteration() { .to(Value.jsonArray(Arrays.asList(jsonArray))) .set("pgJsonbArray") .to(Value.pgJsonbArray(Arrays.asList(jsonArray))) + .set("protoMessageArray") + .to( + Value.protoMessageArray( + Arrays.asList(protoMessageArray), protoMessageVal.getDescriptorForType())) + .set("protoEnumArray") + .to( + Value.protoEnumArray( + Arrays.asList(protoEnumArray), protoEnumVal.getDescriptorForType())) .build(); Struct struct2 = Struct.newBuilder() @@ -173,6 +209,10 @@ public void resultSetIteration() { .to(Timestamp.ofTimeMicroseconds(usecs)) .set("date") .to(Date.fromYearMonthDay(year, month, day)) + .set("protoMessage") + .to(protoMessageVal) + .set("protoEnum") + .to(protoEnumVal) .set("boolArray") .to(Value.boolArray(boolArray)) .set("longArray") @@ -193,6 +233,14 @@ public void resultSetIteration() { .to(Value.jsonArray(Arrays.asList(jsonArray))) .set("pgJsonbArray") .to(Value.pgJsonbArray(Arrays.asList(jsonArray))) + .set("protoMessageArray") + .to( + Value.protoMessageArray( + Arrays.asList(protoMessageArray), protoMessageVal.getDescriptorForType())) + .set("protoEnumArray") + .to( + Value.protoEnumArray( + Arrays.asList(protoEnumArray), protoEnumVal.getDescriptorForType())) .build(); ResultSet rs = ResultSets.forRows(type, Arrays.asList(struct1, struct2)); @@ -259,6 +307,18 @@ public void resultSetIteration() { .isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); assertThat(rs.getDate("date")).isEqualTo(Date.fromYearMonthDay(year, month, day)); assertThat(rs.getValue("date")).isEqualTo(Value.date(Date.fromYearMonthDay(year, month, day))); + + assertEquals(protoMessageVal, rs.getProtoMessage(columnIndex, SingerInfo.getDefaultInstance())); + assertEquals(Value.protoMessage(protoMessageVal), rs.getValue(columnIndex++)); + assertEquals( + protoMessageVal, rs.getProtoMessage("protoMessage", SingerInfo.getDefaultInstance())); + assertEquals(Value.protoMessage(protoMessageVal), rs.getValue("protoMessage")); + + assertEquals(protoEnumVal, rs.getProtoEnum(columnIndex, Genre::forNumber)); + assertEquals(Value.protoEnum(protoEnumVal), rs.getValue(columnIndex++)); + assertEquals(protoEnumVal, rs.getProtoEnum("protoEnum", Genre::forNumber)); + assertEquals(Value.protoEnum(protoEnumVal), rs.getValue("protoEnum")); + assertThat(rs.getBooleanArray(columnIndex)).isEqualTo(boolArray); assertThat(rs.getValue(columnIndex++)).isEqualTo(Value.boolArray(boolArray)); assertThat(rs.getBooleanArray("boolArray")).isEqualTo(boolArray); @@ -305,9 +365,29 @@ public void resultSetIteration() { 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(columnIndex++)); assertEquals(Arrays.asList(jsonArray), rs.getPgJsonbList("pgJsonbArray")); + assertThat(rs.getProtoMessageList(columnIndex, SingerInfo.getDefaultInstance())) + .isEqualTo(Arrays.asList(protoMessageArray)); + assertThat(rs.getValue(columnIndex++)) + .isEqualTo( + Value.protoMessageArray(Arrays.asList(protoMessageArray), SingerInfo.getDescriptor())); + assertThat(rs.getProtoMessageList("protoMessageArray", SingerInfo.getDefaultInstance())) + .isEqualTo(Arrays.asList(protoMessageArray)); + assertThat(rs.getValue("protoMessageArray")) + .isEqualTo( + Value.protoMessageArray(Arrays.asList(protoMessageArray), SingerInfo.getDescriptor())); + + assertThat(rs.getProtoEnumList(columnIndex, Genre::forNumber)) + .isEqualTo(Arrays.asList(protoEnumArray)); + assertThat(rs.getValue(columnIndex)) + .isEqualTo(Value.protoEnumArray(Arrays.asList(protoEnumArray), Genre.getDescriptor())); + assertThat(rs.getProtoEnumList("protoEnumArray", Genre::forNumber)) + .isEqualTo(Arrays.asList(protoEnumArray)); + assertThat(rs.getValue("protoEnumArray")) + .isEqualTo(Value.protoEnumArray(Arrays.asList(protoEnumArray), Genre.getDescriptor())); + assertThat(rs.next()).isTrue(); assertThat(rs.getCurrentRowAsStruct()).isEqualTo(struct2); assertThat(rs.getString(0)).isEqualTo("y"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java new file mode 100644 index 0000000000..56fb82c15d --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java @@ -0,0 +1,1199 @@ +/* + * 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: src/test/resources/com/google/cloud/spanner/singer.proto +package com.google.cloud.spanner; + +public final class SingerProto { + private SingerProto() {} + + public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry) {} + + public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry); + } + /** Protobuf enum {@code spanner.examples.music.Genre} */ + public enum Genre implements com.google.protobuf.ProtocolMessageEnum { + /** POP = 0; */ + POP(0), + /** JAZZ = 1; */ + JAZZ(1), + /** FOLK = 2; */ + FOLK(2), + /** ROCK = 3; */ + ROCK(3), + ; + + /** POP = 0; */ + public static final int POP_VALUE = 0; + /** JAZZ = 1; */ + public static final int JAZZ_VALUE = 1; + /** FOLK = 2; */ + public static final int FOLK_VALUE = 2; + /** ROCK = 3; */ + public static final int ROCK_VALUE = 3; + + public final int getNumber() { + return value; + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + * @deprecated Use {@link #forNumber(int)} instead. + */ + @java.lang.Deprecated + public static Genre valueOf(int value) { + return forNumber(value); + } + + /** + * @param value The numeric wire value of the corresponding enum entry. + * @return The enum associated with the given numeric wire value. + */ + public static Genre forNumber(int value) { + switch (value) { + case 0: + return POP; + case 1: + return JAZZ; + case 2: + return FOLK; + case 3: + return ROCK; + default: + return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap internalGetValueMap() { + return internalValueMap; + } + + private static final com.google.protobuf.Internal.EnumLiteMap internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public Genre findValueByNumber(int number) { + return Genre.forNumber(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() { + return getDescriptor().getValues().get(ordinal()); + } + + public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() { + return getDescriptor(); + } + + public static final com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() { + return com.google.cloud.spanner.SingerProto.getDescriptor().getEnumTypes().get(0); + } + + private static final Genre[] VALUES = values(); + + public static Genre valueOf(com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new java.lang.IllegalArgumentException("EnumValueDescriptor is not for this type."); + } + return VALUES[desc.getIndex()]; + } + + private final int value; + + private Genre(int value) { + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:spanner.examples.music.Genre) + } + + public interface SingerInfoOrBuilder + extends + // @@protoc_insertion_point(interface_extends:spanner.examples.music.SingerInfo) + com.google.protobuf.MessageOrBuilder { + + /** + * optional int64 singer_id = 1; + * + * @return Whether the singerId field is set. + */ + boolean hasSingerId(); + /** + * optional int64 singer_id = 1; + * + * @return The singerId. + */ + long getSingerId(); + + /** + * optional string birth_date = 2; + * + * @return Whether the birthDate field is set. + */ + boolean hasBirthDate(); + /** + * optional string birth_date = 2; + * + * @return The birthDate. + */ + java.lang.String getBirthDate(); + /** + * optional string birth_date = 2; + * + * @return The bytes for birthDate. + */ + com.google.protobuf.ByteString getBirthDateBytes(); + + /** + * optional string nationality = 3; + * + * @return Whether the nationality field is set. + */ + boolean hasNationality(); + /** + * optional string nationality = 3; + * + * @return The nationality. + */ + java.lang.String getNationality(); + /** + * optional string nationality = 3; + * + * @return The bytes for nationality. + */ + com.google.protobuf.ByteString getNationalityBytes(); + + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return Whether the genre field is set. + */ + boolean hasGenre(); + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return The genre. + */ + com.google.cloud.spanner.SingerProto.Genre getGenre(); + } + /** Protobuf type {@code spanner.examples.music.SingerInfo} */ + public static final class SingerInfo extends com.google.protobuf.GeneratedMessageV3 + implements + // @@protoc_insertion_point(message_implements:spanner.examples.music.SingerInfo) + SingerInfoOrBuilder { + private static final long serialVersionUID = 0L; + // Use SingerInfo.newBuilder() to construct. + private SingerInfo(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + + private SingerInfo() { + birthDate_ = ""; + nationality_ = ""; + genre_ = 0; + } + + @java.lang.Override + @SuppressWarnings({"unused"}) + protected java.lang.Object newInstance(UnusedPrivateParameter unused) { + return new SingerInfo(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet getUnknownFields() { + return this.unknownFields; + } + + private SingerInfo( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: + { + bitField0_ |= 0x00000001; + singerId_ = input.readInt64(); + break; + } + case 18: + { + com.google.protobuf.ByteString bs = input.readBytes(); + bitField0_ |= 0x00000002; + birthDate_ = bs; + break; + } + case 26: + { + com.google.protobuf.ByteString bs = input.readBytes(); + bitField0_ |= 0x00000004; + nationality_ = bs; + break; + } + case 32: + { + int rawValue = input.readEnum(); + @SuppressWarnings("deprecation") + com.google.cloud.spanner.SingerProto.Genre value = + com.google.cloud.spanner.SingerProto.Genre.valueOf(rawValue); + if (value == null) { + unknownFields.mergeVarintField(4, rawValue); + } else { + bitField0_ |= 0x00000008; + genre_ = rawValue; + } + break; + } + default: + { + if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException(e).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.cloud.spanner.SingerProto + .internal_static_spanner_examples_music_SingerInfo_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spanner.SingerProto + .internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spanner.SingerProto.SingerInfo.class, + com.google.cloud.spanner.SingerProto.SingerInfo.Builder.class); + } + + private int bitField0_; + public static final int SINGER_ID_FIELD_NUMBER = 1; + private long singerId_; + /** + * optional int64 singer_id = 1; + * + * @return Whether the singerId field is set. + */ + @java.lang.Override + public boolean hasSingerId() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * optional int64 singer_id = 1; + * + * @return The singerId. + */ + @java.lang.Override + public long getSingerId() { + return singerId_; + } + + public static final int BIRTH_DATE_FIELD_NUMBER = 2; + private volatile java.lang.Object birthDate_; + /** + * optional string birth_date = 2; + * + * @return Whether the birthDate field is set. + */ + @java.lang.Override + public boolean hasBirthDate() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * optional string birth_date = 2; + * + * @return The birthDate. + */ + @java.lang.Override + public java.lang.String getBirthDate() { + java.lang.Object ref = birthDate_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + birthDate_ = s; + } + return s; + } + } + /** + * optional string birth_date = 2; + * + * @return The bytes for birthDate. + */ + @java.lang.Override + public com.google.protobuf.ByteString getBirthDateBytes() { + java.lang.Object ref = birthDate_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + birthDate_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int NATIONALITY_FIELD_NUMBER = 3; + private volatile java.lang.Object nationality_; + /** + * optional string nationality = 3; + * + * @return Whether the nationality field is set. + */ + @java.lang.Override + public boolean hasNationality() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * optional string nationality = 3; + * + * @return The nationality. + */ + @java.lang.Override + public java.lang.String getNationality() { + java.lang.Object ref = nationality_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + nationality_ = s; + } + return s; + } + } + /** + * optional string nationality = 3; + * + * @return The bytes for nationality. + */ + @java.lang.Override + public com.google.protobuf.ByteString getNationalityBytes() { + java.lang.Object ref = nationality_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + nationality_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + public static final int GENRE_FIELD_NUMBER = 4; + private int genre_; + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return Whether the genre field is set. + */ + @java.lang.Override + public boolean hasGenre() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return The genre. + */ + @java.lang.Override + public com.google.cloud.spanner.SingerProto.Genre getGenre() { + @SuppressWarnings("deprecation") + com.google.cloud.spanner.SingerProto.Genre result = + com.google.cloud.spanner.SingerProto.Genre.valueOf(genre_); + return result == null ? com.google.cloud.spanner.SingerProto.Genre.POP : result; + } + + private byte memoizedIsInitialized = -1; + + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { + if (((bitField0_ & 0x00000001) != 0)) { + output.writeInt64(1, singerId_); + } + if (((bitField0_ & 0x00000002) != 0)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 2, birthDate_); + } + if (((bitField0_ & 0x00000004) != 0)) { + com.google.protobuf.GeneratedMessageV3.writeString(output, 3, nationality_); + } + if (((bitField0_ & 0x00000008) != 0)) { + output.writeEnum(4, genre_); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeInt64Size(1, singerId_); + } + if (((bitField0_ & 0x00000002) != 0)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, birthDate_); + } + if (((bitField0_ & 0x00000004) != 0)) { + size += com.google.protobuf.GeneratedMessageV3.computeStringSize(3, nationality_); + } + if (((bitField0_ & 0x00000008) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeEnumSize(4, genre_); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof com.google.cloud.spanner.SingerProto.SingerInfo)) { + return super.equals(obj); + } + com.google.cloud.spanner.SingerProto.SingerInfo other = + (com.google.cloud.spanner.SingerProto.SingerInfo) obj; + + if (hasSingerId() != other.hasSingerId()) return false; + if (hasSingerId()) { + if (getSingerId() != other.getSingerId()) return false; + } + if (hasBirthDate() != other.hasBirthDate()) return false; + if (hasBirthDate()) { + if (!getBirthDate().equals(other.getBirthDate())) return false; + } + if (hasNationality() != other.hasNationality()) return false; + if (hasNationality()) { + if (!getNationality().equals(other.getNationality())) return false; + } + if (hasGenre() != other.hasGenre()) return false; + if (hasGenre()) { + if (genre_ != other.genre_) return false; + } + if (!unknownFields.equals(other.unknownFields)) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (hasSingerId()) { + hash = (37 * hash) + SINGER_ID_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getSingerId()); + } + if (hasBirthDate()) { + hash = (37 * hash) + BIRTH_DATE_FIELD_NUMBER; + hash = (53 * hash) + getBirthDate().hashCode(); + } + if (hasNationality()) { + hash = (37 * hash) + NATIONALITY_FIELD_NUMBER; + hash = (53 * hash) + getNationality().hashCode(); + } + if (hasGenre()) { + hash = (37 * hash) + GENRE_FIELD_NUMBER; + hash = (53 * hash) + genre_; + } + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + java.nio.ByteBuffer data) throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + java.nio.ByteBuffer data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseDelimitedFrom( + java.io.InputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseDelimitedFrom( + java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException( + PARSER, input, extensionRegistry); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + com.google.protobuf.CodedInputStream input) throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3.parseWithIOException( + PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { + return newBuilder(); + } + + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + + public static Builder newBuilder(com.google.cloud.spanner.SingerProto.SingerInfo prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** Protobuf type {@code spanner.examples.music.SingerInfo} */ + public static final class Builder + extends com.google.protobuf.GeneratedMessageV3.Builder + implements + // @@protoc_insertion_point(builder_implements:spanner.examples.music.SingerInfo) + com.google.cloud.spanner.SingerProto.SingerInfoOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { + return com.google.cloud.spanner.SingerProto + .internal_static_spanner_examples_music_SingerInfo_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return com.google.cloud.spanner.SingerProto + .internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable + .ensureFieldAccessorsInitialized( + com.google.cloud.spanner.SingerProto.SingerInfo.class, + com.google.cloud.spanner.SingerProto.SingerInfo.Builder.class); + } + + // Construct using com.google.cloud.spanner.SingerProto.SingerInfo.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder(com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders) {} + } + + @java.lang.Override + public Builder clear() { + super.clear(); + singerId_ = 0L; + bitField0_ = (bitField0_ & ~0x00000001); + birthDate_ = ""; + bitField0_ = (bitField0_ & ~0x00000002); + nationality_ = ""; + bitField0_ = (bitField0_ & ~0x00000004); + genre_ = 0; + bitField0_ = (bitField0_ & ~0x00000008); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { + return com.google.cloud.spanner.SingerProto + .internal_static_spanner_examples_music_SingerInfo_descriptor; + } + + @java.lang.Override + public com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstanceForType() { + return com.google.cloud.spanner.SingerProto.SingerInfo.getDefaultInstance(); + } + + @java.lang.Override + public com.google.cloud.spanner.SingerProto.SingerInfo build() { + com.google.cloud.spanner.SingerProto.SingerInfo result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public com.google.cloud.spanner.SingerProto.SingerInfo buildPartial() { + com.google.cloud.spanner.SingerProto.SingerInfo result = + new com.google.cloud.spanner.SingerProto.SingerInfo(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) != 0)) { + result.singerId_ = singerId_; + to_bitField0_ |= 0x00000001; + } + if (((from_bitField0_ & 0x00000002) != 0)) { + to_bitField0_ |= 0x00000002; + } + result.birthDate_ = birthDate_; + if (((from_bitField0_ & 0x00000004) != 0)) { + to_bitField0_ |= 0x00000004; + } + result.nationality_ = nationality_; + if (((from_bitField0_ & 0x00000008) != 0)) { + to_bitField0_ |= 0x00000008; + } + result.genre_ = genre_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return super.clone(); + } + + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.setField(field, value); + } + + @java.lang.Override + public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) { + return super.clearField(field); + } + + @java.lang.Override + public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return super.clearOneof(oneof); + } + + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, + java.lang.Object value) { + return super.setRepeatedField(field, index, value); + } + + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + return super.addRepeatedField(field, value); + } + + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof com.google.cloud.spanner.SingerProto.SingerInfo) { + return mergeFrom((com.google.cloud.spanner.SingerProto.SingerInfo) other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(com.google.cloud.spanner.SingerProto.SingerInfo other) { + if (other == com.google.cloud.spanner.SingerProto.SingerInfo.getDefaultInstance()) + return this; + if (other.hasSingerId()) { + setSingerId(other.getSingerId()); + } + if (other.hasBirthDate()) { + bitField0_ |= 0x00000002; + birthDate_ = other.birthDate_; + onChanged(); + } + if (other.hasNationality()) { + bitField0_ |= 0x00000004; + nationality_ = other.nationality_; + onChanged(); + } + if (other.hasGenre()) { + setGenre(other.getGenre()); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + com.google.cloud.spanner.SingerProto.SingerInfo parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = + (com.google.cloud.spanner.SingerProto.SingerInfo) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + + private int bitField0_; + + private long singerId_; + /** + * optional int64 singer_id = 1; + * + * @return Whether the singerId field is set. + */ + @java.lang.Override + public boolean hasSingerId() { + return ((bitField0_ & 0x00000001) != 0); + } + /** + * optional int64 singer_id = 1; + * + * @return The singerId. + */ + @java.lang.Override + public long getSingerId() { + return singerId_; + } + /** + * optional int64 singer_id = 1; + * + * @param value The singerId to set. + * @return This builder for chaining. + */ + public Builder setSingerId(long value) { + bitField0_ |= 0x00000001; + singerId_ = value; + onChanged(); + return this; + } + /** + * optional int64 singer_id = 1; + * + * @return This builder for chaining. + */ + public Builder clearSingerId() { + bitField0_ = (bitField0_ & ~0x00000001); + singerId_ = 0L; + onChanged(); + return this; + } + + private java.lang.Object birthDate_ = ""; + /** + * optional string birth_date = 2; + * + * @return Whether the birthDate field is set. + */ + public boolean hasBirthDate() { + return ((bitField0_ & 0x00000002) != 0); + } + /** + * optional string birth_date = 2; + * + * @return The birthDate. + */ + public java.lang.String getBirthDate() { + java.lang.Object ref = birthDate_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + birthDate_ = s; + } + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string birth_date = 2; + * + * @return The bytes for birthDate. + */ + public com.google.protobuf.ByteString getBirthDateBytes() { + java.lang.Object ref = birthDate_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + birthDate_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string birth_date = 2; + * + * @param value The birthDate to set. + * @return This builder for chaining. + */ + public Builder setBirthDate(java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + birthDate_ = value; + onChanged(); + return this; + } + /** + * optional string birth_date = 2; + * + * @return This builder for chaining. + */ + public Builder clearBirthDate() { + bitField0_ = (bitField0_ & ~0x00000002); + birthDate_ = getDefaultInstance().getBirthDate(); + onChanged(); + return this; + } + /** + * optional string birth_date = 2; + * + * @param value The bytes for birthDate to set. + * @return This builder for chaining. + */ + public Builder setBirthDateBytes(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + birthDate_ = value; + onChanged(); + return this; + } + + private java.lang.Object nationality_ = ""; + /** + * optional string nationality = 3; + * + * @return Whether the nationality field is set. + */ + public boolean hasNationality() { + return ((bitField0_ & 0x00000004) != 0); + } + /** + * optional string nationality = 3; + * + * @return The nationality. + */ + public java.lang.String getNationality() { + java.lang.Object ref = nationality_; + if (!(ref instanceof java.lang.String)) { + com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + nationality_ = s; + } + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string nationality = 3; + * + * @return The bytes for nationality. + */ + public com.google.protobuf.ByteString getNationalityBytes() { + java.lang.Object ref = nationality_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + nationality_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string nationality = 3; + * + * @param value The nationality to set. + * @return This builder for chaining. + */ + public Builder setNationality(java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + nationality_ = value; + onChanged(); + return this; + } + /** + * optional string nationality = 3; + * + * @return This builder for chaining. + */ + public Builder clearNationality() { + bitField0_ = (bitField0_ & ~0x00000004); + nationality_ = getDefaultInstance().getNationality(); + onChanged(); + return this; + } + /** + * optional string nationality = 3; + * + * @param value The bytes for nationality to set. + * @return This builder for chaining. + */ + public Builder setNationalityBytes(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + nationality_ = value; + onChanged(); + return this; + } + + private int genre_ = 0; + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return Whether the genre field is set. + */ + @java.lang.Override + public boolean hasGenre() { + return ((bitField0_ & 0x00000008) != 0); + } + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return The genre. + */ + @java.lang.Override + public com.google.cloud.spanner.SingerProto.Genre getGenre() { + @SuppressWarnings("deprecation") + com.google.cloud.spanner.SingerProto.Genre result = + com.google.cloud.spanner.SingerProto.Genre.valueOf(genre_); + return result == null ? com.google.cloud.spanner.SingerProto.Genre.POP : result; + } + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @param value The genre to set. + * @return This builder for chaining. + */ + public Builder setGenre(com.google.cloud.spanner.SingerProto.Genre value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000008; + genre_ = value.getNumber(); + onChanged(); + return this; + } + /** + * optional .spanner.examples.music.Genre genre = 4; + * + * @return This builder for chaining. + */ + public Builder clearGenre() { + bitField0_ = (bitField0_ & ~0x00000008); + genre_ = 0; + onChanged(); + return this; + } + + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFields(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + // @@protoc_insertion_point(builder_scope:spanner.examples.music.SingerInfo) + } + + // @@protoc_insertion_point(class_scope:spanner.examples.music.SingerInfo) + private static final com.google.cloud.spanner.SingerProto.SingerInfo DEFAULT_INSTANCE; + + static { + DEFAULT_INSTANCE = new com.google.cloud.spanner.SingerProto.SingerInfo(); + } + + public static com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + @java.lang.Deprecated + public static final com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + @java.lang.Override + public SingerInfo parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new SingerInfo(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_spanner_examples_music_SingerInfo_descriptor; + private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + return descriptor; + } + + private static com.google.protobuf.Descriptors.FileDescriptor descriptor; + + static { + java.lang.String[] descriptorData = { + "\n\014singer.proto\022\026spanner.examples.music\"v" + + "\n\nSingerInfo\022\021\n\tsinger_id\030\001 \001(\003\022\022\n\nbirth" + + "_date\030\002 \001(\t\022\023\n\013nationality\030\003 \001(\t\022,\n\005genr" + + "e\030\004 \001(\0162\035.spanner.examples.music.Genre*." + + "\n\005Genre\022\007\n\003POP\020\000\022\010\n\004JAZZ\020\001\022\010\n\004FOLK\020\002\022\010\n\004" + + "ROCK\020\003B)\n\030com.google.cloud.spannerB\013Sing" + + "erProtoP\000" + }; + descriptor = + com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( + descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] {}); + internal_static_spanner_examples_music_SingerInfo_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable = + new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_spanner_examples_music_SingerInfo_descriptor, + new java.lang.String[] { + "SingerId", "BirthDate", "Nationality", "Genre", + }); + } + + // @@protoc_insertion_point(outer_class_scope) +} 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 48fe282b12..11b708ed48 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 @@ -40,6 +40,7 @@ private abstract static class ScalarTypeTester { private final Type.Code expectedCode; private final TypeCode expectedTypeCode; private final TypeAnnotationCode expectedTypeAnnotationCode; + private String protoTypeFqn = ""; ScalarTypeTester(Type.Code expectedCode, TypeCode expectedTypeCode) { this(expectedCode, expectedTypeCode, TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED); @@ -54,12 +55,17 @@ private abstract static class ScalarTypeTester { this.expectedTypeAnnotationCode = expectedTypeAnnotationCode; } + ScalarTypeTester(Type.Code expectedCode, TypeCode expectedTypeCode, String protoTypeFqn) { + this(expectedCode, expectedTypeCode); + this.protoTypeFqn = protoTypeFqn; + } + abstract Type newType(); void test() { Type t = newType(); assertThat(t.getCode()).isEqualTo(expectedCode); - assertThat(newType()).isSameInstanceAs(t); // Interned. + assertThat(newType()).isEqualTo(t); // Interned. // String form is deliberately the same as the corresponding type enum in the public API. if (expectedTypeAnnotationCode != TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED) { assertThat(t.toString()) @@ -72,13 +78,13 @@ void test() { com.google.spanner.v1.Type proto = t.toProto(); assertThat(proto.getCode()).isEqualTo(expectedTypeCode); assertThat(proto.getTypeAnnotation()).isEqualTo(expectedTypeAnnotationCode); + assertThat(proto.getProtoTypeFqn()).isEqualTo(protoTypeFqn); assertThat(proto.hasArrayElementType()).isFalse(); assertThat(proto.hasStructType()).isFalse(); // Round trip. Type fromProto = Type.fromProto(proto); assertThat(fromProto).isEqualTo(t); - assertThat(fromProto).isSameInstanceAs(t); reserializeAndAssert(t); } @@ -174,6 +180,26 @@ Type newType() { }.test(); } + @Test + public void proto() { + new ScalarTypeTester(Type.Code.PROTO, TypeCode.PROTO, "com.google.temp") { + @Override + Type newType() { + return Type.proto("com.google.temp"); + } + }.test(); + } + + @Test + public void protoEnum() { + new ScalarTypeTester(Type.Code.ENUM, TypeCode.ENUM, "com.google.temp.enum") { + @Override + Type newType() { + return Type.protoEnum("com.google.temp.enum"); + } + }.test(); + } + @Test public void timestamp() { new ScalarTypeTester(Type.Code.TIMESTAMP, TypeCode.TIMESTAMP) { @@ -199,6 +225,7 @@ abstract static class ArrayTypeTester { private final TypeCode expectedElementTypeCode; private final TypeAnnotationCode expectedTypeAnnotationCode; private final boolean expectInterned; + private String protoTypeFqn = ""; ArrayTypeTester( Type.Code expectedElementCode, TypeCode expectedElementTypeCode, boolean expectInterned) { @@ -209,6 +236,19 @@ abstract static class ArrayTypeTester { expectInterned); } + ArrayTypeTester( + Type.Code expectedElementCode, + TypeCode expectedElementTypeCode, + String protoTypeFqn, + boolean expectInterned) { + this( + expectedElementCode, + expectedElementTypeCode, + TypeAnnotationCode.TYPE_ANNOTATION_CODE_UNSPECIFIED, + expectInterned); + this.protoTypeFqn = protoTypeFqn; + } + ArrayTypeTester( Type.Code expectedElementCode, TypeCode expectedElementTypeCode, @@ -358,6 +398,26 @@ Type newElementType() { }.test(); } + @Test + public void protoArray() { + new ArrayTypeTester(Type.Code.PROTO, TypeCode.PROTO, "com.google.temp", false) { + @Override + Type newElementType() { + return Type.proto("com.google.temp"); + } + }.test(); + } + + @Test + public void protoEnumArray() { + new ArrayTypeTester(Type.Code.ENUM, TypeCode.ENUM, "com.google.temp.enum", false) { + @Override + Type newElementType() { + return Type.protoEnum("com.google.temp.enum"); + } + }.test(); + } + @Test public void arrayOfArray() { new ArrayTypeTester(Type.Code.ARRAY, TypeCode.ARRAY, false /* not interned */) { 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 8da5c0322d..d50814e84d 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 @@ -25,6 +25,12 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.EnumDescriptor; +import com.google.protobuf.ProtocolMessageEnum; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -43,6 +49,8 @@ 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"; + private static final String PROTO_MESSAGE_METHOD_NAME = "protoMessage"; + private static final String PROTO_ENUM_METHOD_NAME = "protoEnum"; private static final String BYTES_BASE64_METHOD_NAME = "bytesFromBase64"; public static final String DEFAULT_PG_NUMERIC = "1.23"; @@ -125,7 +133,9 @@ public void reflection() } } else if (binderMethod.getParameterTypes().length == 1) { // Test unary null. - if (!binderMethod.getParameterTypes()[0].isPrimitive()) { + if (!binderMethod.getParameterTypes()[0].isPrimitive() + && (!method.getName().equalsIgnoreCase(PROTO_MESSAGE_METHOD_NAME) + && !method.getName().equalsIgnoreCase(PROTO_ENUM_METHOD_NAME))) { if (method.getName().equalsIgnoreCase(JSON_METHOD_NAME)) { // Special case for json to change the method from ValueBinder.to(String) to // ValueBinder.to(Value) @@ -147,7 +157,6 @@ public void reflection() } Value expected = (Value) method.invoke(Value.class, (Object) null); assertThat(lastValue).isEqualTo(expected); - assertThat(binder.to(expected)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); } @@ -180,6 +189,27 @@ public void reflection() Value expected = (Value) method.invoke(Value.class, defaultObject); assertThat(lastValue).isEqualTo(expected); + assertThat(binder.to(expected)).isEqualTo(lastReturnValue); + assertThat(lastValue).isEqualTo(expected); + } else if (binderMethod.getParameterTypes().length == 2 + && (method.getName().contains(PROTO_MESSAGE_METHOD_NAME) + || method.getName().contains(PROTO_ENUM_METHOD_NAME))) { + // Test unary null. + Object firstArgument = null; + if (binderMethod.getParameterTypes()[0].isPrimitive()) { + firstArgument = 0; + } + + Object secondArgument = "com.proto.example"; + if (binderMethod.getParameterTypes()[1] == Descriptor.class) { + secondArgument = SingerInfo.getDescriptor(); + } else if (binderMethod.getParameterTypes()[1] == EnumDescriptor.class) { + secondArgument = Genre.getDescriptor(); + } + assertThat(binderMethod.invoke(binder, firstArgument, secondArgument)) + .isEqualTo(lastReturnValue); + Value expected = (Value) method.invoke(Value.class, firstArgument, secondArgument); + assertThat(lastValue).isEqualTo(expected); assertThat(binder.to(expected)).isEqualTo(lastReturnValue); assertThat(lastValue).isEqualTo(expected); } else { @@ -247,6 +277,14 @@ public static BigDecimal defaultBigDecimal() { return BigDecimal.valueOf(123, 2); } + public static AbstractMessage defaultAbstractMessage() { + return SingerInfo.newBuilder().setSingerId(323).build(); + } + + public static ProtocolMessageEnum defaultProtocolMessageEnum() { + return Genre.FOLK; + } + public static String defaultString() { return "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 bf806795fa..5176013cf3 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 @@ -33,6 +33,8 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbstractResultSet.LazyByteArray; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.Type.StructField; import com.google.common.base.Strings; import com.google.common.collect.ForwardingList; @@ -674,6 +676,56 @@ public void dateNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void protoMessage() { + SingerInfo singerInfo = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); + Value v = Value.protoMessage(singerInfo); + assertThat(v.getType()).isEqualTo(Type.proto(SingerInfo.getDescriptor().getFullName())); + assertThat(v.isNull()).isFalse(); + assertThat(v.getProtoMessage(SingerInfo.getDefaultInstance())).isEqualTo(singerInfo); + assertThat(v.getBytes().toByteArray()).isEqualTo(singerInfo.toByteArray()); + } + + @Test + public void protoMessageNull() { + Value v = Value.protoMessage(null, SingerInfo.getDescriptor().getFullName()); + assertThat(v.getType()).isEqualTo(Type.proto(SingerInfo.getDescriptor().getFullName())); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> { + v.getProtoMessage(SingerInfo.getDefaultInstance()); + }); + assertThat(e.getMessage()).contains("null value"); + } + + @Test + public void protoEnum() { + Genre genre = Genre.FOLK; + Value v = Value.protoEnum(genre); + assertThat(v.getType()).isEqualTo(Type.protoEnum(Genre.getDescriptor().getFullName())); + assertThat(v.isNull()).isFalse(); + assertThat(v.getInt64()).isEqualTo(genre.getNumber()); + assertEquals(genre, v.getProtoEnum(Genre::forNumber)); + } + + @Test + public void protoEnumNull() { + Value v = Value.protoEnum(null, Genre.getDescriptor().getFullName()); + assertThat(v.getType()).isEqualTo(Type.protoEnum(Genre.getDescriptor().getFullName())); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> { + v.getProtoEnum(Genre::forNumber); + }); + assertThat(e.getMessage()).contains("null value"); + } + @Test public void boolArray() { Value v = Value.boolArray(new boolean[] {true, false}); @@ -1137,6 +1189,70 @@ public void dateArrayNull() { assertEquals("NULL", v.getAsString()); } + @Test + public void protoMessageArray() { + SingerInfo singerInfo1 = SingerInfo.newBuilder().setSingerId(111).setGenre(Genre.FOLK).build(); + SingerInfo singerInfo2 = SingerInfo.newBuilder().setSingerId(222).build(); + Value v = + Value.protoMessageArray( + Arrays.asList(singerInfo1, null, singerInfo2), SingerInfo.getDescriptor()); + assertThat(v.getType()) + .isEqualTo(Type.array(Type.proto(SingerInfo.getDescriptor().getFullName()))); + assertThat(v.isNull()).isFalse(); + assertThat(v.getProtoMessageArray(SingerInfo.getDefaultInstance())) + .containsExactly(singerInfo1, null, singerInfo2); + assertThat(v.getBytesArray()) + .containsExactly( + ByteArray.copyFrom(singerInfo1.toByteArray()), + null, + ByteArray.copyFrom(singerInfo2.toByteArray())); + } + + @Test + public void protoMessageNullArray() { + Value v = Value.protoMessageArray(null, SingerInfo.getDescriptor()); + assertThat(v.getType()) + .isEqualTo(Type.array(Type.proto(SingerInfo.getDescriptor().getFullName()))); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> { + v.getProtoMessageArray(SingerInfo.getDefaultInstance()); + }); + assertThat(e.getMessage()).contains("null value"); + } + + @Test + public void protoEnumArray() { + Genre genre1 = Genre.ROCK; + Genre genre2 = Genre.JAZZ; + Value v = Value.protoEnumArray(Arrays.asList(genre1, null, genre2), Genre.getDescriptor()); + assertThat(v.getType()) + .isEqualTo(Type.array(Type.protoEnum(Genre.getDescriptor().getFullName()))); + assertThat(v.isNull()).isFalse(); + assertThat(v.getProtoEnumArray(Genre::forNumber)).containsExactly(genre1, null, genre2); + assertThat(v.getInt64Array()) + .containsExactly((long) genre1.getNumber(), null, (long) genre2.getNumber()); + } + + @Test + public void protoEnumNullArray() { + Value v = Value.protoEnumArray(null, Genre.getDescriptor()); + assertThat(v.getType()) + .isEqualTo(Type.array(Type.protoEnum(Genre.getDescriptor().getFullName()))); + assertThat(v.isNull()).isTrue(); + assertThat(v.toString()).isEqualTo(NULL_STRING); + IllegalStateException e = + assertThrows( + IllegalStateException.class, + () -> { + v.getProtoEnumArray(Genre::forNumber); + }); + assertThat(e.getMessage()).contains("null value"); + } + @Test public void struct() { Struct struct = Struct.newBuilder().set("f1").to("v1").set("f2").to(30).build(); 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 index 1f3f59e96a..4e8fb0cfcb 100644 --- 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 @@ -29,6 +29,8 @@ import com.google.cloud.spanner.AbortedException; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Struct.Builder; @@ -65,6 +67,10 @@ public class ChecksumResultSetTest { .to(Value.json("{\"color\":\"red\",\"value\":\"#ff0\"}")) .set("pgJsonbVal") .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#00f\"}")) + .set("protoMessageVal") + .to(SingerInfo.newBuilder().setSingerId(23).build()) + .set("protoEnumVal") + .to(Genre.JAZZ) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom("bytes".getBytes(StandardCharsets.UTF_8)))) .set("timestamp") @@ -104,6 +110,15 @@ public class ChecksumResultSetTest { .to( Value.pgJsonbArray( Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "[]"))) + .set("protoMessageArray") + .to( + Value.protoMessageArray( + Arrays.asList( + SingerInfo.newBuilder().setSingerId(23).build(), + SingerInfo.getDefaultInstance()), + SingerInfo.getDescriptor())) + .set("protoEnumArray") + .to(Value.protoEnumArray(Arrays.asList(Genre.JAZZ, Genre.ROCK), Genre.getDescriptor())) .build(); @Test @@ -118,6 +133,10 @@ public void testRetry() { Type.StructField.of("stringVal", Type.string()), Type.StructField.of("jsonVal", Type.json()), Type.StructField.of("pgJsonbVal", Type.pgJsonb()), + Type.StructField.of( + "protoMessageVal", Type.proto(SingerInfo.getDescriptor().getFullName())), + Type.StructField.of( + "protoEnumVal", Type.protoEnum(Genre.getDescriptor().getFullName())), Type.StructField.of("byteVal", Type.bytes()), Type.StructField.of("timestamp", Type.timestamp()), Type.StructField.of("date", Type.date()), @@ -131,7 +150,12 @@ public void testRetry() { 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()))); + Type.StructField.of("pgJsonbArray", Type.array(Type.pgJsonb())), + Type.StructField.of( + "protoMessageArray", + Type.array(Type.proto(SingerInfo.getDescriptor().getFullName()))), + Type.StructField.of( + "protoEnumArray", Type.array(Type.protoEnum(Genre.getDescriptor().getFullName())))); Struct rowNonNullValues = Struct.newBuilder() .set("boolVal") @@ -150,6 +174,10 @@ public void testRetry() { .to(Value.json("{\"color\":\"red\",\"value\":\"#f00\"}")) .set("pgJsonbVal") .to(Value.pgJsonb("{\"color\":\"red\",\"value\":\"#f00\"}")) + .set("protoMessageVal") + .to(SingerInfo.newBuilder().setSingerId(98).setNationality("C1").build()) + .set("protoEnumVal") + .to(Genre.POP) .set("byteVal") .to(Value.bytes(ByteArray.copyFrom("test".getBytes(StandardCharsets.UTF_8)))) .set("timestamp") @@ -192,6 +220,15 @@ public void testRetry() { .to( Value.pgJsonbArray( Arrays.asList("{\"color\":\"red\",\"value\":\"#f00\"}", null, "{}"))) + .set("protoMessageArray") + .to( + Value.protoMessageArray( + Arrays.asList( + SingerInfo.newBuilder().setSingerId(11).setNationality("C1").build(), + SingerInfo.getDefaultInstance()), + SingerInfo.getDescriptor())) + .set("protoEnumArray") + .to(Value.protoEnumArray(Arrays.asList(Genre.POP, Genre.ROCK), Genre.getDescriptor())) .build(); Struct rowNullValues = Struct.newBuilder() @@ -211,6 +248,10 @@ public void testRetry() { .to(Value.json(null)) .set("pgJsonbVal") .to(Value.pgJsonb(null)) + .set("protoMessageVal") + .to(Value.protoMessage(null, SingerInfo.getDescriptor().getFullName())) + .set("protoEnumVal") + .to(Value.protoEnum(null, Genre.getDescriptor().getFullName())) .set("byteVal") .to((ByteArray) null) .set("timestamp") @@ -239,6 +280,10 @@ public void testRetry() { .toJsonArray(null) .set("pgJsonbArray") .toPgJsonbArray(null) + .set("protoMessageArray") + .to(Value.protoMessageArray(null, SingerInfo.getDescriptor())) + .set("protoEnumArray") + .to(Value.protoEnumArray(null, Genre.getDescriptor())) .build(); ParsedStatement parsedStatement = mock(ParsedStatement.class); 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 346055060a..1e4f96d156 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 @@ -25,15 +25,20 @@ import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ProtocolMessageEnum; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -123,7 +128,7 @@ private void callMethods( boolean exception = false; int numberOfParameters = method.getParameterTypes().length; Class firstParameterType = null; - if (numberOfParameters == 1) { + if (numberOfParameters >= 1) { firstParameterType = method.getParameterTypes()[0]; } try { @@ -140,8 +145,32 @@ private void callMethods( fail("unknown parameter type"); } break; + case 2: + Class secondParameterType = method.getParameterTypes()[1]; + Object firstArgument = null, secondArgument = null; + + if (firstParameterType == String.class) { + firstArgument = "test"; + } else if (firstParameterType == int.class) { + firstArgument = 0; + } + + if (secondParameterType == Function.class) { + Function lambdaFunction = + (val) -> Genre.forNumber(val.intValue()); + secondArgument = lambdaFunction; + } else if (secondParameterType == AbstractMessage.class) { + secondArgument = SingerInfo.getDefaultInstance(); + } + + if (firstArgument != null && secondArgument != null) { + method.invoke(subject, firstArgument, secondArgument); + } else { + fail("unknown parameter type"); + } + break; default: - fail("method with more than 1 parameter is unknown"); + fail("method with more than 2 parameters is unknown"); } } catch (InvocationTargetException e) { if (e.getCause().getClass().equals(expectedException)) { @@ -261,6 +290,25 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getPgJsonbList("test2"); verify(delegate).getPgJsonbList("test2"); + subject.getProtoMessage(0, SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessage(0, SingerInfo.getDefaultInstance()); + subject.getProtoMessage("test0", SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessage("test0", SingerInfo.getDefaultInstance()); + subject.getProtoMessageList(0, SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessageList(0, SingerInfo.getDefaultInstance()); + subject.getProtoMessageList("test0", SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessageList("test0", SingerInfo.getDefaultInstance()); + + Function lambdaFunction = Genre::forNumber; + subject.getProtoEnum(0, lambdaFunction); + verify(delegate).getProtoEnum(0, lambdaFunction); + subject.getProtoEnum("test0", lambdaFunction); + verify(delegate).getProtoEnum("test0", lambdaFunction); + subject.getProtoEnumList(0, lambdaFunction); + verify(delegate).getProtoEnumList(0, lambdaFunction); + subject.getProtoEnumList("test0", lambdaFunction); + verify(delegate).getProtoEnumList("test0", lambdaFunction); + 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 f8415fb00e..69ae89b2ee 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 @@ -19,6 +19,8 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.cloud.spanner.Dialect; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.common.io.BaseEncoding; import com.google.protobuf.ListValue; import com.google.protobuf.NullValue; @@ -32,6 +34,9 @@ import com.google.spanner.v1.TypeAnnotationCode; import com.google.spanner.v1.TypeCode; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Random; /** @@ -40,73 +45,103 @@ */ public class RandomResultSetGenerator { public static Type[] generateAllTypes(Dialect dialect) { - return new Type[] { - Type.newBuilder().setCode(TypeCode.BOOL).build(), - Type.newBuilder().setCode(TypeCode.INT64).build(), - Type.newBuilder().setCode(TypeCode.FLOAT64).build(), - dialect == Dialect.POSTGRESQL - ? Type.newBuilder() - .setCode(TypeCode.NUMERIC) - .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) - .build() - : Type.newBuilder().setCode(TypeCode.NUMERIC).build(), - Type.newBuilder().setCode(TypeCode.STRING).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(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.BOOL)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.INT64)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType( - dialect == Dialect.POSTGRESQL - ? Type.newBuilder() - .setCode(TypeCode.NUMERIC) - .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) - : Type.newBuilder().setCode(TypeCode.NUMERIC)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType( - dialect == Dialect.POSTGRESQL - ? Type.newBuilder() - .setCode(TypeCode.JSON) - .setTypeAnnotation(TypeAnnotationCode.PG_JSONB) - : Type.newBuilder().setCode(TypeCode.JSON)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) - .build(), - Type.newBuilder() - .setCode(TypeCode.ARRAY) - .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) - .build(), - }; + List types = + new ArrayList( + Arrays.asList( + Type.newBuilder().setCode(TypeCode.BOOL).build(), + Type.newBuilder().setCode(TypeCode.INT64).build(), + Type.newBuilder().setCode(TypeCode.FLOAT64).build(), + dialect == Dialect.POSTGRESQL + ? Type.newBuilder() + .setCode(TypeCode.NUMERIC) + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + .build() + : Type.newBuilder().setCode(TypeCode.NUMERIC).build(), + Type.newBuilder().setCode(TypeCode.STRING).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(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.BOOL)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.INT64)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + dialect == Dialect.POSTGRESQL + ? Type.newBuilder() + .setCode(TypeCode.NUMERIC) + .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC) + : Type.newBuilder().setCode(TypeCode.NUMERIC)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + dialect == Dialect.POSTGRESQL + ? Type.newBuilder() + .setCode(TypeCode.JSON) + .setTypeAnnotation(TypeAnnotationCode.PG_JSONB) + : Type.newBuilder().setCode(TypeCode.JSON)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) + .build())); + + // appendProtoTypes(types, dialect); + Type[] typeArray = new Type[types.size()]; + typeArray = types.toArray(typeArray); + return typeArray; + } + + /** To append Proto & Enum types * */ + private static void appendProtoTypes(List types, Dialect dialect) { + if (dialect == Dialect.GOOGLE_STANDARD_SQL) { + types.add(Type.newBuilder().setCode(TypeCode.PROTO).setProtoTypeFqn("testProto").build()); + types.add(Type.newBuilder().setCode(TypeCode.ENUM).setProtoTypeFqn("testEnum").build()); + types.add( + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + Type.newBuilder() + .setCode(TypeCode.PROTO) + .setProtoTypeFqn(SingerInfo.getDescriptor().getFullName())) + .build()); + types.add( + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType( + Type.newBuilder() + .setCode(TypeCode.ENUM) + .setProtoTypeFqn(Genre.getDescriptor().getFullName())) + .build()); + } } public static ResultSetMetadata generateAllTypesMetadata(Type[] types) { @@ -171,6 +206,7 @@ private void setRandomValue(Value.Builder builder, Type type) { break; case STRING: case BYTES: + case PROTO: byte[] bytes = new byte[random.nextInt(200)]; random.nextBytes(bytes); builder.setStringValue(BaseEncoding.base64().encode(bytes)); @@ -199,6 +235,7 @@ private void setRandomValue(Value.Builder builder, Type type) { } break; case INT64: + case ENUM: builder.setStringValue(String.valueOf(random.nextLong())); break; case TIMESTAMP: diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 358e8faf28..0f083fd1e5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -40,6 +40,8 @@ import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Statement; @@ -53,6 +55,7 @@ import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType; import com.google.cloud.spanner.connection.UnitOfWork.CallType; +import com.google.protobuf.ProtocolMessageEnum; import com.google.rpc.RetryInfo; import com.google.spanner.v1.ResultSetStats; import io.grpc.Metadata; @@ -508,13 +511,25 @@ public void testChecksumResultSet() { "[{\"color\":\"red\",\"value\":\"#f00\"},{\"color\":\"green\",\"value\":\"#0f0\"},{\"color\":\"blue\",\"value\":\"#00f\"},{\"color\":\"cyan\",\"value\":\"#0ff\"},{\"color\":\"magenta\",\"value\":\"#f0f\"},{\"color\":\"yellow\",\"value\":\"#ff0\"},{\"color\":\"black\",\"value\":\"#000\"}]"; String emptyArrayJson = "[]"; String simpleJson = "{\"color\":\"red\",\"value\":\"#f00\"}"; + SingerInfo protoMessageVal = + SingerInfo.newBuilder() + .setSingerId(111) + .setNationality("COUNTRY1") + .setGenre(Genre.FOLK) + .build(); + ProtocolMessageEnum protoEnumVal = Genre.ROCK; ResultSet delegate1 = ResultSets.forRows( Type.struct( StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json())), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), Arrays.asList( Struct.newBuilder() .set("ID") @@ -525,6 +540,10 @@ public void testChecksumResultSet() { .to(BigDecimal.valueOf(550, 2)) .set("JSON") .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) .build(), Struct.newBuilder() .set("ID") @@ -535,6 +554,10 @@ public void testChecksumResultSet() { .to(BigDecimal.valueOf(750, 2)) .set("JSON") .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) .build())); ChecksumResultSet rs1 = transaction.createChecksumResultSet(delegate1, parsedStatement, AnalyzeMode.NONE); @@ -544,7 +567,12 @@ public void testChecksumResultSet() { StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json())), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), Arrays.asList( Struct.newBuilder() .set("ID") @@ -555,6 +583,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("5.50")) .set("JSON") .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) .build(), Struct.newBuilder() .set("ID") @@ -565,6 +597,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("7.50")) .set("JSON") .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) .build())); ChecksumResultSet rs2 = transaction.createChecksumResultSet(delegate2, parsedStatement, AnalyzeMode.NONE); @@ -575,7 +611,12 @@ public void testChecksumResultSet() { StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json())), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), Arrays.asList( Struct.newBuilder() .set("ID") @@ -586,6 +627,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("7.50")) .set("JSON") .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) .build(), Struct.newBuilder() .set("ID") @@ -596,6 +641,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("5.50")) .set("JSON") .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) .build())); ChecksumResultSet rs3 = transaction.createChecksumResultSet(delegate3, parsedStatement, AnalyzeMode.NONE); @@ -607,7 +656,12 @@ public void testChecksumResultSet() { StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string()), StructField.of("AMOUNT", Type.numeric()), - StructField.of("JSON", Type.json())), + StructField.of("JSON", Type.json()), + StructField.of( + "PROTO", Type.proto(protoMessageVal.getDescriptorForType().getFullName())), + StructField.of( + "PROTOENUM", + Type.protoEnum(protoEnumVal.getDescriptorForType().getFullName()))), Arrays.asList( Struct.newBuilder() .set("ID") @@ -618,6 +672,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("5.50")) .set("JSON") .to(Value.json(simpleJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(protoEnumVal) .build(), Struct.newBuilder() .set("ID") @@ -628,6 +686,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("7.50")) .set("JSON") .to(Value.json(arrayJson)) + .set("PROTO") + .to(protoMessageVal) + .set("PROTOENUM") + .to(Genre.JAZZ) .build(), Struct.newBuilder() .set("ID") @@ -638,6 +700,10 @@ public void testChecksumResultSet() { .to(new BigDecimal("9.99")) .set("JSON") .to(Value.json(emptyArrayJson)) + .set("PROTO") + .to(null, SingerInfo.getDescriptor()) + .set("PROTOENUM") + .to(Genre.POP) .build())); ChecksumResultSet rs4 = transaction.createChecksumResultSet(delegate4, parsedStatement, AnalyzeMode.NONE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java index 3f69c2171e..bbb3467514 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java @@ -26,16 +26,21 @@ import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.ResultSets; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.Type.StructField; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ProtocolMessageEnum; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -164,7 +169,7 @@ private void callMethods( boolean exception = false; int numberOfParameters = method.getParameterTypes().length; Class firstParameterType = null; - if (numberOfParameters == 1) { + if (numberOfParameters >= 1) { firstParameterType = method.getParameterTypes()[0]; } try { @@ -181,8 +186,32 @@ private void callMethods( fail("unknown parameter type"); } break; + case 2: + Class secondParameterType = method.getParameterTypes()[1]; + Object firstArgument = null, secondArgument = null; + + if (firstParameterType == String.class) { + firstArgument = "test"; + } else if (firstParameterType == int.class) { + firstArgument = 0; + } + + if (secondParameterType == Function.class) { + Function lambdaFunction = + (val) -> Genre.forNumber(val.intValue()); + secondArgument = lambdaFunction; + } else if (secondParameterType == AbstractMessage.class) { + secondArgument = SingerInfo.getDefaultInstance(); + } + + if (firstArgument != null && secondArgument != null) { + method.invoke(subject, firstArgument, secondArgument); + } else { + fail("unknown parameter type"); + } + break; default: - fail("method with more than 1 parameter is unknown"); + fail("method with more than 2 parameters is unknown"); } } catch (InvocationTargetException e) { if (e.getCause().getClass().equals(expectedException)) { @@ -296,6 +325,25 @@ public void testValidMethodCall() throws IllegalArgumentException { subject.getJsonList("test2"); verify(delegate).getJsonList("test2"); + subject.getProtoMessage(0, SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessage(0, SingerInfo.getDefaultInstance()); + subject.getProtoMessage("test0", SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessage("test0", SingerInfo.getDefaultInstance()); + subject.getProtoMessageList(0, SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessageList(0, SingerInfo.getDefaultInstance()); + subject.getProtoMessageList("test0", SingerInfo.getDefaultInstance()); + verify(delegate).getProtoMessageList("test0", SingerInfo.getDefaultInstance()); + + Function lambdaFunction = Genre::forNumber; + subject.getProtoEnum(0, lambdaFunction); + verify(delegate).getProtoEnum(0, lambdaFunction); + subject.getProtoEnum("test0", lambdaFunction); + verify(delegate).getProtoEnum("test0", lambdaFunction); + subject.getProtoEnumList(0, lambdaFunction); + verify(delegate).getProtoEnumList(0, lambdaFunction); + subject.getProtoEnumList("test0", lambdaFunction); + verify(delegate).getProtoEnumList("test0", lambdaFunction); + subject.getStructList(0); subject.getStructList("test0"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java new file mode 100644 index 0000000000..c1e06f9ad7 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java @@ -0,0 +1,402 @@ +/* + * 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 com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeFalse; + +import com.google.cloud.ByteArray; +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.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SingerProto.Genre; +import com.google.cloud.spanner.SingerProto.SingerInfo; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.InvalidProtocolBufferException.InvalidWireTypeException; +import com.google.protobuf.ProtocolMessageEnum; +import com.google.spanner.admin.database.v1.Backup; +import com.google.spanner.admin.database.v1.GetDatabaseDdlResponse; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +// Integration Tests to test DDL, DML and DQL for Proto Columns and Enums +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITProtoColumnTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static DatabaseId databaseID; + private static DatabaseAdminClient dbAdminClient; + private static DatabaseClient databaseClient; + + @BeforeClass + public static void setUpDatabase() throws Exception { + assumeFalse( + "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + RemoteSpannerHelper testHelper = env.getTestHelper(); + databaseID = DatabaseId.of(testHelper.getInstanceId(), testHelper.getUniqueDatabaseId()); + dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); + createDatabase(); + databaseClient = testHelper.getClient().getDatabaseClient(databaseID); + } + + public static void createDatabase() throws Exception { + InputStream in = + ITProtoColumnTest.class + .getClassLoader() + .getResourceAsStream("com/google/cloud/spanner/descriptors.pb"); + final Database databaseToCreate = + dbAdminClient.newDatabaseBuilder(databaseID).setProtoDescriptors(in).build(); + final Database createdDatabase = + dbAdminClient + .createDatabase( + databaseToCreate, + Arrays.asList( + "CREATE PROTO BUNDLE (" + + "spanner.examples.music.SingerInfo," + + "spanner.examples.music.Genre," + + ")", + "CREATE TABLE Singers (" + + " SingerId INT64 NOT NULL," + + " FirstName STRING(1024)," + + " LastName STRING(1024)," + + " SingerInfo spanner.examples.music.SingerInfo," + + " SingerGenre spanner.examples.music.Genre," + + " SingerNationality STRING(1024) AS (SingerInfo.nationality) STORED," + + " ) PRIMARY KEY (SingerNationality, SingerGenre)", + "CREATE TABLE Types (" + + " RowID INT64 NOT NULL," + + " Int64a INT64," + + " Bytes BYTES(MAX)," + + " Int64Array ARRAY," + + " BytesArray ARRAY," + + " ProtoMessage spanner.examples.music.SingerInfo," + + " ProtoEnum spanner.examples.music.Genre," + + " ProtoMessageArray ARRAY," + + " ProtoEnumArray ARRAY," + + " ) PRIMARY KEY (RowID)", + "CREATE INDEX SingerByNationalityAndGenre ON Singers(SingerNationality, SingerGenre)" + + " STORING (SingerId, FirstName, LastName)")) + .get(5, TimeUnit.MINUTES); + + assertEquals(databaseID.getDatabase(), createdDatabase.getId().getDatabase()); + + GetDatabaseDdlResponse response = + dbAdminClient.getDatabaseDdlResponse( + databaseID.getInstanceId().getInstance(), databaseID.getDatabase()); + assertNotNull(response.getProtoDescriptors()); + in.close(); + } + + @AfterClass + public static void afterClass() throws Exception { + try { + if (!isUsingEmulator()) { + dbAdminClient.dropDatabase( + databaseID.getInstanceId().getInstance(), databaseID.getDatabase()); + } + } catch (Exception e) { + System.err.println( + "Failed to drop database " + + dbAdminClient + .getDatabase(databaseID.getInstanceId().getInstance(), databaseID.getDatabase()) + .getId() + + ", skipping...: " + + e.getMessage()); + } + } + + @After + public void after() throws Exception { + databaseClient.write(ImmutableList.of(Mutation.delete("Types", KeySet.all()))); + databaseClient.write(ImmutableList.of(Mutation.delete("Singers", KeySet.all()))); + } + + /** + * Test to check data update and read queries on Proto Messages, Proto Enums and their arrays. + * Test also checks for compatability between following types: 1. Proto Messages and Bytes 2. + * Proto Enums and Int64 + */ + @Test + public void testProtoColumnsUpdateAndRead() { + assumeFalse( + "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + SingerInfo singerInfo = + SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); + ByteArray singerInfoBytes = ByteArray.copyFrom(singerInfo.toByteArray()); + + Genre genre = Genre.JAZZ; + long genreConst = genre.getNumber(); + + List singerInfoList = + Arrays.asList(singerInfo, null, SingerInfo.getDefaultInstance()); + List singerInfoBytesList = + Arrays.asList( + singerInfoBytes, + null, + ByteArray.copyFrom(SingerInfo.getDefaultInstance().toByteArray())); + + List enumList = Arrays.asList(Genre.FOLK, null, Genre.ROCK); + List enumConstList = + Arrays.asList((long) Genre.FOLK_VALUE, null, (long) Genre.ROCK_VALUE); + + // Inserting two rows with same data except rowID as it's used as PK. + databaseClient.write( + ImmutableList.of( + Mutation.newInsertOrUpdateBuilder("Types") + .set("RowID") + .to(1) + .set("Int64a") + .to(genreConst) + .set("Bytes") + .to(singerInfoBytes) + .set("Int64Array") + .toInt64Array(enumConstList) + .set("BytesArray") + .toBytesArray(singerInfoBytesList) + .set("ProtoMessage") + .to(singerInfo) + .set("ProtoEnum") + .to(genre) + .set("ProtoMessageArray") + .toProtoMessageArray(singerInfoList, SingerInfo.getDescriptor()) + .set("ProtoEnumArray") + .toProtoEnumArray(enumList, Genre.getDescriptor()) + .build(), + // Inter Compatability check between ProtoMessages/Bytes and Int64/Enum. + Mutation.newInsertOrUpdateBuilder("Types") + .set("RowID") + .to(2) + .set("Int64a") + .to(genre) + .set("Bytes") + .to(singerInfo) + .set("Int64Array") + .toProtoEnumArray(enumList, Genre.getDescriptor()) + .set("BytesArray") + .toProtoMessageArray(singerInfoList, SingerInfo.getDescriptor()) + .set("ProtoMessage") + .to(singerInfoBytes) + .set("ProtoEnum") + .to(genreConst) + .set("ProtoMessageArray") + .toBytesArray(singerInfoBytesList) + .set("ProtoEnumArray") + .toInt64Array(enumConstList) + .build())); + + try (ResultSet resultSet = + databaseClient.singleUse().executeQuery(Statement.of("SELECT * FROM " + "Types"))) { + + for (int i = 0; i < 2; i++) { + resultSet.next(); + assertEquals(i + 1, resultSet.getLong("RowID")); + assertEquals(genreConst, resultSet.getLong("Int64a")); + assertEquals(singerInfoBytes, resultSet.getBytes("Bytes")); + assertEquals(enumConstList, resultSet.getLongList("Int64Array")); + assertEquals(singerInfoBytesList, resultSet.getBytesList("BytesArray")); + assertEquals( + singerInfo, resultSet.getProtoMessage("ProtoMessage", SingerInfo.getDefaultInstance())); + assertEquals(genre, resultSet.getProtoEnum("ProtoEnum", Genre::forNumber)); + assertEquals( + singerInfoList, + resultSet.getProtoMessageList("ProtoMessageArray", SingerInfo.getDefaultInstance())); + assertEquals(enumList, resultSet.getProtoEnumList("ProtoEnumArray", Genre::forNumber)); + + // Check compatability between Proto Messages and Bytes + assertEquals(singerInfoBytes, resultSet.getBytes("ProtoMessage")); + assertEquals( + singerInfo, resultSet.getProtoMessage("Bytes", SingerInfo.getDefaultInstance())); + + assertEquals(singerInfoBytesList, resultSet.getBytesList("ProtoMessageArray")); + assertEquals( + singerInfoList, + resultSet.getProtoMessageList("BytesArray", SingerInfo.getDefaultInstance())); + + // Check compatability between Proto Enum and Int64 + assertEquals(genreConst, resultSet.getLong("ProtoEnum")); + assertEquals(genre, resultSet.getProtoEnum("Int64a", Genre::forNumber)); + + assertEquals(enumConstList, resultSet.getLongList("ProtoEnumArray")); + assertEquals(enumList, resultSet.getProtoEnumList("Int64Array", Genre::forNumber)); + } + } + } + + // Test to check Parameterized Queries, Primary Keys and Indexes. + @Test + public void testProtoColumnsDMLParameterizedQueriesPKAndIndexes() { + assumeFalse( + "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + + SingerInfo singerInfo1 = + SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); + Genre genre1 = Genre.FOLK; + + SingerInfo singerInfo2 = + SingerInfo.newBuilder().setSingerId(2).setNationality("Country2").build(); + Genre genre2 = Genre.JAZZ; + + databaseClient + .readWriteTransaction() + .run( + transaction -> { + Statement statement1 = + Statement.newBuilder( + "INSERT INTO Singers (SingerId, FirstName, LastName, SingerInfo, SingerGenre) VALUES (1, \"FirstName1\", \"LastName1\", @singerInfo, @singerGenre)") + .bind("singerInfo") + .to(singerInfo1) + .bind("singerGenre") + .to(genre1) + .build(); + + Statement statement2 = + Statement.newBuilder( + "INSERT INTO Singers (SingerId, FirstName, LastName, SingerInfo, SingerGenre) VALUES (2, \"FirstName2\", \"LastName2\", @singerInfo, @singerGenre)") + .bind("singerInfo") + .to(singerInfo2) + .bind("singerGenre") + .to(genre2) + .build(); + + transaction.batchUpdate(Arrays.asList(statement1, statement2)); + return null; + }); + + // Read all rows based on Proto Message field and Proto Enum Primary key column values + ResultSet resultSet1 = + databaseClient + .singleUse() + .read( + "Singers", + KeySet.newBuilder() + .addKey(Key.of("Country1", Genre.FOLK)) + .addKey(Key.of("Country2", Genre.JAZZ)) + .build(), + Arrays.asList("SingerId", "FirstName", "LastName", "SingerInfo", "SingerGenre")); + + resultSet1.next(); + assertEquals(1, resultSet1.getLong("SingerId")); + assertEquals("FirstName1", resultSet1.getString("FirstName")); + assertEquals("LastName1", resultSet1.getString("LastName")); + assertEquals( + singerInfo1, resultSet1.getProtoMessage("SingerInfo", SingerInfo.getDefaultInstance())); + assertEquals(genre1, resultSet1.getProtoEnum("SingerGenre", Genre::forNumber)); + + resultSet1.next(); + assertEquals(2, resultSet1.getLong("SingerId")); + assertEquals("FirstName2", resultSet1.getString("FirstName")); + assertEquals("LastName2", resultSet1.getString("LastName")); + assertEquals( + singerInfo2, resultSet1.getProtoMessage("SingerInfo", SingerInfo.getDefaultInstance())); + assertEquals(genre2, resultSet1.getProtoEnum("SingerGenre", Genre::forNumber)); + + // Read rows using Index on Proto Message field and Proto Enum column + ResultSet resultSet2 = + databaseClient + .singleUse() + .readUsingIndex( + "Singers", + "SingerByNationalityAndGenre", + KeySet.singleKey(Key.of("Country2", Genre.JAZZ)), + Arrays.asList("SingerId", "FirstName", "LastName")); + resultSet2.next(); + assertEquals(2, resultSet2.getLong("SingerId")); + assertEquals("FirstName2", resultSet2.getString("FirstName")); + assertEquals("LastName2", resultSet2.getString("LastName")); + + // Filter using Parameterized DQL + ResultSet resultSet3 = + databaseClient + .singleUse() + .executeQuery( + Statement.newBuilder( + "SELECT SingerId, SingerInfo, SingerGenre FROM " + + "Singers WHERE SingerInfo.Nationality=@country AND SingerGenre=@genre") + .bind("country") + .to("Country2") + .bind("genre") + .to(Genre.JAZZ) + .build()); + + resultSet3.next(); + assertEquals(2, resultSet1.getLong("SingerId")); + assertEquals( + singerInfo2, resultSet1.getProtoMessage("SingerInfo", SingerInfo.getDefaultInstance())); + assertEquals(genre2, resultSet1.getProtoEnum("SingerGenre", Genre::forNumber)); + } + + // Test the exception in case Invalid protocol message object is provided while deserializing the + // data. + @Test + public void testProtoMessageDeserializationError() { + assumeFalse( + "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); + + SingerInfo singerInfo = + SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); + + databaseClient.write( + ImmutableList.of( + Mutation.newInsertOrUpdateBuilder("Types") + .set("RowID") + .to(1) + .set("ProtoMessage") + .to(singerInfo) + .build())); + + ResultSet resultSet = + databaseClient + .singleUse() + .read("Types", KeySet.all(), Collections.singletonList("ProtoMessage")); + resultSet.next(); + + SpannerException e = + assertThrows( + SpannerException.class, + () -> resultSet.getProtoMessage("ProtoMessage", Backup.getDefaultInstance())); + + // Underlying cause is InvalidWireTypeException + assertEquals(InvalidWireTypeException.class, e.getCause().getClass()); + } +} diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb new file mode 100644 index 0000000000000000000000000000000000000000..3ebb79420b3ffd2ca3b3b57433a4a10bfa22b675 GIT binary patch literal 251 zcmd=3!N|o^oSB!NTBKJ{lwXoBBvxFIn3o6SrdA~87UZNB>*bafXC^DnXXN4v1}pT; zOUoCM=Hi5Ci_c7vU{qk#U=HGd2zaIl$#QWeWfqlW#HS>dq)IRWWjTX5!6Gg|0U-r0 z?!3g3%>2B>oXnC+31+Z7vXGE57i)TIUQwz93s8>FNLCNKqx9TCih>|&we+}H!F(Zh lF6IFL009Oe4lWii$EYX)Mi9%*-^W{k3B(HWclH)w1^^+RM@0Yt literal 0 HcmV?d00001 diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto new file mode 100644 index 0000000000..b62b48ed67 --- /dev/null +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto @@ -0,0 +1,21 @@ +syntax = "proto2"; + +package spanner.examples.music; + +option java_package = "com.google.cloud.spanner"; +option java_outer_classname = "SingerProto"; +option java_multiple_files = false; + +message SingerInfo { + optional int64 singer_id = 1; + optional string birth_date = 2; + optional string nationality = 3; + optional Genre genre = 4; +} + +enum Genre { + POP = 0; + JAZZ = 1; + FOLK = 2; + ROCK = 3; +}