3535import com .google .cloud .bigtable .data .v2 .models .sql .ResultSet ;
3636import com .google .cloud .bigtable .data .v2 .models .sql .SqlType ;
3737import com .google .cloud .bigtable .data .v2 .models .sql .StructReader ;
38+ import com .google .protobuf .AbstractMessage ;
3839import com .google .protobuf .ByteString ;
40+ import com .google .protobuf .DescriptorProtos .FileDescriptorProto ;
41+ import com .google .protobuf .DescriptorProtos .FileDescriptorSet ;
42+ import com .google .protobuf .Descriptors .Descriptor ;
43+ import com .google .protobuf .Descriptors .DescriptorValidationException ;
44+ import com .google .protobuf .Descriptors .EnumDescriptor ;
45+ import com .google .protobuf .Descriptors .FileDescriptor ;
46+ import com .google .protobuf .DynamicMessage ;
47+ import com .google .protobuf .ProtocolMessageEnum ;
3948import java .time .Instant ;
49+ import java .util .ArrayList ;
50+ import java .util .HashMap ;
4051import java .util .List ;
4152import java .util .concurrent .ExecutionException ;
4253
4354public class ResultSetSerializer {
44- public static ExecuteQueryResult toExecuteQueryResult (ResultSet resultSet )
55+
56+ // This is a helper enum to satisfy the type constraints of {@link StructReader#getProtoEnum}.
57+ private static class DummyEnum implements ProtocolMessageEnum {
58+ private final int value ;
59+ private final EnumDescriptor descriptor ;
60+
61+ private DummyEnum (int value , EnumDescriptor descriptor ) {
62+ this .value = value ;
63+ this .descriptor = descriptor ;
64+ }
65+
66+ @ Override
67+ public int getNumber () {
68+ return value ;
69+ }
70+
71+ @ Override
72+ public com .google .protobuf .Descriptors .EnumValueDescriptor getValueDescriptor () {
73+ return descriptor .findValueByNumber (value );
74+ }
75+
76+ @ Override
77+ public com .google .protobuf .Descriptors .EnumDescriptor getDescriptorForType () {
78+ return descriptor ;
79+ }
80+ }
81+
82+ /**
83+ * A map of all known message descriptors, keyed by their fully qualified name (e.g.,
84+ * "my.package.MyMessage").
85+ */
86+ private final java .util .Map <String , Descriptor > messageDescriptorMap ;
87+
88+ /**
89+ * A map of all known enum descriptors, keyed by their fully qualified name (e.g.,
90+ * "my.package.MyEnum").
91+ */
92+ private final java .util .Map <String , EnumDescriptor > enumDescriptorMap ;
93+
94+ /**
95+ * Helper function to recursively adds a message descriptor and all its nested types to the map.
96+ */
97+ private void populateDescriptorMapsRecursively (Descriptor descriptor ) {
98+ messageDescriptorMap .put (descriptor .getFullName (), descriptor );
99+
100+ for (EnumDescriptor nestedEnum : descriptor .getEnumTypes ()) {
101+ enumDescriptorMap .put (nestedEnum .getFullName (), nestedEnum );
102+ }
103+ for (Descriptor nestedMessage : descriptor .getNestedTypes ()) {
104+ populateDescriptorMapsRecursively (nestedMessage );
105+ }
106+ }
107+
108+ /**
109+ * Creates a serializer with a descriptor cache built from the provided FileDescriptorSet. This is
110+ * useful for handling PROTO or ENUM types that require schema lookup.
111+ *
112+ * @param descriptorSet A set containing one or more .proto file definitions and all their
113+ * non-standard dependencies.
114+ * @throws IllegalArgumentException if the descriptorSet contains unresolvable dependencies.
115+ */
116+ public ResultSetSerializer (FileDescriptorSet descriptorSet ) throws IllegalArgumentException {
117+ this .messageDescriptorMap = new HashMap <>();
118+ this .enumDescriptorMap = new HashMap <>();
119+
120+ // Java's dynamic descriptor loading requires manual dependency resolution.
121+ // We must build a DAG and build files only after their dependencies are built.
122+ java .util .Map <String , FileDescriptorProto > pendingProtos = new HashMap <>();
123+ java .util .Map <String , FileDescriptor > builtDescriptors = new HashMap <>();
124+ for (FileDescriptorProto fileDescriptorProto : descriptorSet .getFileList ()) {
125+ pendingProtos .put (fileDescriptorProto .getName (), fileDescriptorProto );
126+ }
127+
128+ // Keep looping as long as we are making progress.
129+ int filesBuiltThisPass ;
130+ do {
131+ filesBuiltThisPass = 0 ;
132+ // Use a copy of the values, to avoid modifying the pendingProtos map while iterating it.
133+ List <FileDescriptorProto > protosToBuild = new ArrayList <>(pendingProtos .values ());
134+
135+ for (FileDescriptorProto proto : protosToBuild ) {
136+ boolean allDependenciesMet = true ;
137+ List <FileDescriptor > dependencies = new ArrayList <>();
138+
139+ for (String dependencyName : proto .getDependencyList ()) {
140+ FileDescriptor dependency = builtDescriptors .get (dependencyName );
141+ if (dependency != null ) {
142+ // Dependency is already built, add it.
143+ dependencies .add (dependency );
144+ } else if (pendingProtos .containsKey (dependencyName )) {
145+ // Dependency is not yet built, we must wait.
146+ allDependenciesMet = false ;
147+ break ;
148+ } else {
149+ // Dependency is not in our set. We assume it's a well-known type (e.g.,
150+ // google/protobuf/timestamp.proto) that buildFrom() can find and link automatically.
151+ }
152+ }
153+
154+ if (allDependenciesMet ) {
155+ try {
156+ // All dependencies are met, we can build this file.
157+ FileDescriptor fileDescriptor =
158+ FileDescriptor .buildFrom (proto , dependencies .toArray (new FileDescriptor [0 ]));
159+
160+ builtDescriptors .put (fileDescriptor .getName (), fileDescriptor );
161+ pendingProtos .remove (proto .getName ());
162+ filesBuiltThisPass ++;
163+
164+ // Now, populate both message and enum maps with all messages/enums in this file.
165+ for (EnumDescriptor enumDescriptor : fileDescriptor .getEnumTypes ()) {
166+ enumDescriptorMap .put (enumDescriptor .getFullName (), enumDescriptor );
167+ }
168+ for (Descriptor messageDescriptor : fileDescriptor .getMessageTypes ()) {
169+ populateDescriptorMapsRecursively (messageDescriptor );
170+ }
171+ } catch (DescriptorValidationException e ) {
172+ throw new IllegalArgumentException (
173+ "Failed to build descriptor for " + proto .getName (), e );
174+ }
175+ }
176+ }
177+ } while (filesBuiltThisPass > 0 );
178+
179+ // Throw if we finished looping but still have pending protos.
180+ if (!pendingProtos .isEmpty ()) {
181+ throw new IllegalArgumentException (
182+ "Unresolvable or circular dependencies found for: " + pendingProtos .keySet ());
183+ }
184+ }
185+
186+ public ExecuteQueryResult toExecuteQueryResult (ResultSet resultSet )
45187 throws ExecutionException , InterruptedException {
46188 ExecuteQueryResult .Builder resultBuilder = ExecuteQueryResult .newBuilder ();
47189 for (ColumnMetadata columnMetadata : resultSet .getMetadata ().getColumns ()) {
@@ -64,24 +206,28 @@ public static ExecuteQueryResult toExecuteQueryResult(ResultSet resultSet)
64206 return resultBuilder .build ();
65207 }
66208
67- private static Value toProtoValue (Object value , SqlType <?> type ) {
209+ private Value toProtoValue (Object value , SqlType <?> type ) {
68210 if (value == null ) {
69211 return Value .getDefaultInstance ();
70212 }
71213
72214 Value .Builder valueBuilder = Value .newBuilder ();
73215 switch (type .getCode ()) {
74216 case BYTES :
75- case PROTO :
76217 valueBuilder .setBytesValue ((ByteString ) value );
77218 break ;
219+ case PROTO :
220+ valueBuilder .setBytesValue (((AbstractMessage ) value ).toByteString ());
221+ break ;
78222 case STRING :
79223 valueBuilder .setStringValue ((String ) value );
80224 break ;
81225 case INT64 :
82- case ENUM :
83226 valueBuilder .setIntValue ((Long ) value );
84227 break ;
228+ case ENUM :
229+ valueBuilder .setIntValue (((ProtocolMessageEnum ) value ).getNumber ());
230+ break ;
85231 case FLOAT32 :
86232 valueBuilder .setFloatValue ((Float ) value );
87233 break ;
@@ -151,7 +297,7 @@ private static Value toProtoValue(Object value, SqlType<?> type) {
151297 return valueBuilder .build ();
152298 }
153299
154- private static Object getColumn (StructReader struct , int fieldIndex , SqlType <?> fieldType ) {
300+ private Object getColumn (StructReader struct , int fieldIndex , SqlType <?> fieldType ) {
155301 if (struct .isNull (fieldIndex )) {
156302 return null ;
157303 }
@@ -162,17 +308,35 @@ private static Object getColumn(StructReader struct, int fieldIndex, SqlType<?>
162308 case BOOL :
163309 return struct .getBoolean (fieldIndex );
164310 case BYTES :
165- case PROTO :
166311 return struct .getBytes (fieldIndex );
312+ case PROTO :
313+ SchemalessProto protoType = (SchemalessProto ) fieldType ;
314+ Descriptor descriptor = messageDescriptorMap .get (protoType .getMessageName ());
315+ if (descriptor == null ) {
316+ throw new IllegalArgumentException (
317+ "Descriptor for message " + protoType .getMessageName () + " could not be found" );
318+ }
319+ return struct .getProtoMessage (fieldIndex , DynamicMessage .getDefaultInstance (descriptor ));
167320 case DATE :
168321 return struct .getDate (fieldIndex );
169322 case FLOAT32 :
170323 return struct .getFloat (fieldIndex );
171324 case FLOAT64 :
172325 return struct .getDouble (fieldIndex );
173326 case INT64 :
174- case ENUM :
175327 return struct .getLong (fieldIndex );
328+ case ENUM :
329+ SchemalessEnum enumType = (SchemalessEnum ) fieldType ;
330+ EnumDescriptor enumDescriptor = enumDescriptorMap .get (enumType .getEnumName ());
331+ if (enumDescriptor == null ) {
332+ throw new IllegalArgumentException (
333+ "Descriptor for enum " + enumType .getEnumName () + " could not be found" );
334+ }
335+ // We need to extract the integer value of the enum. `getProtoEnum` is the only
336+ // available method, but it is designed for static enum types. To work around this,
337+ // we can pass a lambda that constructs our DummyEnum with the captured integer value
338+ // and the descriptor from the outer scope.
339+ return struct .getProtoEnum (fieldIndex , number -> new DummyEnum (number , enumDescriptor ));
176340 case MAP :
177341 return struct .getMap (fieldIndex , (SqlType .Map <?, ?>) fieldType );
178342 case STRING :
0 commit comments