@@ -25,6 +25,8 @@ import (
2525 "google.golang.org/protobuf/reflect/protodesc"
2626 "google.golang.org/protobuf/reflect/protoreflect"
2727 "google.golang.org/protobuf/types/descriptorpb"
28+ "google.golang.org/protobuf/types/known/durationpb"
29+ "google.golang.org/protobuf/types/known/timestamppb"
2830 "google.golang.org/protobuf/types/known/wrapperspb"
2931)
3032
@@ -114,6 +116,21 @@ var bqTypeToWrapperMap = map[storagepb.TableFieldSchema_Type]string{
114116// filename used by well known types proto
115117var wellKnownTypesWrapperName = "google/protobuf/wrappers.proto"
116118
119+ // filename used by timestamp and duration proto
120+ var extraWellKnownTypesPerTypeName = map [string ]struct {
121+ name string
122+ fileDescriptor * descriptorpb.FileDescriptorProto
123+ }{
124+ ".google.protobuf.Timestamp" : {
125+ name : "google/protobuf/timestamp.proto" ,
126+ fileDescriptor : protodesc .ToFileDescriptorProto (timestamppb .File_google_protobuf_timestamp_proto ),
127+ },
128+ ".google.protobuf.Duration" : {
129+ name : "google/protobuf/duration.proto" ,
130+ fileDescriptor : protodesc .ToFileDescriptorProto (durationpb .File_google_protobuf_duration_proto ),
131+ },
132+ }
133+
117134var rangeTypesPrefix = "rangemessage_range_"
118135
119136// dependencyCache is used to reduce the number of unique messages we generate by caching based on the tableschema.
@@ -180,7 +197,7 @@ func (dm *dependencyCache) add(schema *storagepb.TableSchema, descriptor protore
180197 return nil
181198}
182199
183- func (dm * dependencyCache ) addRangeByElementType (typ storagepb.TableFieldSchema_Type , useProto3 bool ) (protoreflect.MessageDescriptor , error ) {
200+ func (dm * dependencyCache ) addRangeByElementType (typ storagepb.TableFieldSchema_Type , cfg * customConfig ) (protoreflect.MessageDescriptor , error ) {
184201 if md , present := dm .rangeTypes [typ ]; present {
185202 // already added, do nothing.
186203 return md , nil
@@ -212,7 +229,7 @@ func (dm *dependencyCache) addRangeByElementType(typ storagepb.TableFieldSchema_
212229 // we put the range types outside the hierarchical namespace as they're effectively BQ-specific well-known types.
213230 msgTypeName := fmt .Sprintf ("%s%s" , rangeTypesPrefix , strings .ToLower (typ .String ()))
214231 // use a new dependency cache, as we don't want to taint the main one due to matching schema
215- md , err := storageSchemaToDescriptorInternal (ts , msgTypeName , newDependencyCache (), useProto3 )
232+ md , err := storageSchemaToDescriptorInternal (ts , msgTypeName , newDependencyCache (), cfg )
216233 if err != nil {
217234 return nil , fmt .Errorf ("failed to generate range descriptor %q: %v" , msgTypeName , err )
218235 }
@@ -230,22 +247,34 @@ func (dm *dependencyCache) getRange(typ storagepb.TableFieldSchema_Type) protore
230247
231248// StorageSchemaToProto2Descriptor builds a protoreflect.Descriptor for a given table schema using proto2 syntax.
232249func StorageSchemaToProto2Descriptor (inSchema * storagepb.TableSchema , scope string ) (protoreflect.Descriptor , error ) {
233- dc := newDependencyCache ()
234- // TODO: b/193064992 tracks support for wrapper types. In the interim, disable wrapper usage.
235- return storageSchemaToDescriptorInternal (inSchema , scope , dc , false )
250+ return StorageSchemaToProtoDescriptorWithOptions (inSchema , scope , withProto2 ())
236251}
237252
238253// StorageSchemaToProto3Descriptor builds a protoreflect.Descriptor for a given table schema using proto3 syntax.
239254//
240255// NOTE: Currently the write API doesn't yet support proto3 behaviors (default value, wrapper types, etc), but this is provided for
241256// completeness.
242257func StorageSchemaToProto3Descriptor (inSchema * storagepb.TableSchema , scope string ) (protoreflect.Descriptor , error ) {
258+ return StorageSchemaToProtoDescriptorWithOptions (inSchema , scope , withProto3 ())
259+ }
260+
261+ // StorageSchemaToProtoDescriptorWithOptions builds a protoreflect.Descriptor for a given table schema with
262+ // extra configuration options. Uses proto2 syntax by default.
263+ func StorageSchemaToProtoDescriptorWithOptions (inSchema * storagepb.TableSchema , scope string , opts ... ProtoConversionOption ) (protoreflect.Descriptor , error ) {
243264 dc := newDependencyCache ()
244- return storageSchemaToDescriptorInternal (inSchema , scope , dc , true )
265+ cfg := & customConfig {
266+ useProto3 : false ,
267+ protoMappingOverrides : protoMappingOverrides {},
268+ }
269+ for _ , opt := range opts {
270+ opt .applyCustomClientOpt (cfg )
271+ }
272+ // TODO: b/193064992 tracks support for wrapper types. In the interim, disable wrapper usage.
273+ return storageSchemaToDescriptorInternal (inSchema , scope , dc , cfg )
245274}
246275
247276// Internal implementation of the conversion code.
248- func storageSchemaToDescriptorInternal (inSchema * storagepb.TableSchema , scope string , cache * dependencyCache , useProto3 bool ) (protoreflect.MessageDescriptor , error ) {
277+ func storageSchemaToDescriptorInternal (inSchema * storagepb.TableSchema , scope string , cache * dependencyCache , cfg * customConfig ) (protoreflect.MessageDescriptor , error ) {
249278 if inSchema == nil {
250279 return nil , newConversionError (scope , fmt .Errorf ("no input schema was provided" ))
251280 }
@@ -277,14 +306,14 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
277306 deps = append (deps , foundDesc .ParentFile ())
278307 }
279308 // Construct field descriptor for the message.
280- fdp := tableFieldSchemaToFieldDescriptorProto (f , fNumber , string (foundDesc .FullName ()), useProto3 )
309+ fdp := tableFieldSchemaToFieldDescriptorProto (f , fNumber , string (foundDesc .FullName ()), cfg )
281310 fields = append (fields , fdp )
282311 } else {
283312 // Wrap the current struct's fields in a TableSchema outer message, and then build the submessage.
284313 ts := & storagepb.TableSchema {
285314 Fields : f .GetFields (),
286315 }
287- desc , err := storageSchemaToDescriptorInternal (ts , currentScope , cache , useProto3 )
316+ desc , err := storageSchemaToDescriptorInternal (ts , currentScope , cache , cfg )
288317 if err != nil {
289318 return nil , newConversionError (currentScope , fmt .Errorf ("couldn't convert message: %w" , err ))
290319 }
@@ -295,7 +324,7 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
295324 if err != nil {
296325 return nil , newConversionError (currentScope , fmt .Errorf ("failed to add descriptor to dependency cache: %w" , err ))
297326 }
298- fdp := tableFieldSchemaToFieldDescriptorProto (f , fNumber , currentScope , useProto3 )
327+ fdp := tableFieldSchemaToFieldDescriptorProto (f , fNumber , currentScope , cfg )
299328 fields = append (fields , fdp )
300329 }
301330 } else {
@@ -305,7 +334,7 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
305334 if ret == nil {
306335 return nil , fmt .Errorf ("field %q is a RANGE, but doesn't include RangeElementType info" , f .GetName ())
307336 }
308- foundDesc , err := cache .addRangeByElementType (ret .GetType (), useProto3 )
337+ foundDesc , err := cache .addRangeByElementType (ret .GetType (), cfg )
309338 if err != nil {
310339 return nil , err
311340 }
@@ -323,7 +352,7 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
323352 }
324353 }
325354 }
326- fd := tableFieldSchemaToFieldDescriptorProto (f , fNumber , currentScope , useProto3 )
355+ fd := tableFieldSchemaToFieldDescriptorProto (f , fNumber , currentScope , cfg )
327356 fields = append (fields , fd )
328357 }
329358 }
@@ -335,6 +364,11 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
335364
336365 // Use the local dependencies to generate a list of filenames.
337366 depNames := []string {wellKnownTypesWrapperName }
367+ for _ , override := range cfg .protoMappingOverrides {
368+ if dep , found := extraWellKnownTypesPerTypeName [override .TypeName ]; found {
369+ depNames = append (depNames , dep .name )
370+ }
371+ }
338372 for _ , d := range deps {
339373 depNames = append (depNames , d .ParentFile ().Path ())
340374 }
@@ -346,7 +380,7 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
346380 Syntax : proto .String ("proto3" ),
347381 Dependency : depNames ,
348382 }
349- if ! useProto3 {
383+ if ! cfg . useProto3 {
350384 fdp .Syntax = proto .String ("proto2" )
351385 }
352386
@@ -357,6 +391,11 @@ func storageSchemaToDescriptorInternal(inSchema *storagepb.TableSchema, scope st
357391 fdp ,
358392 protodesc .ToFileDescriptorProto (wrapperspb .File_google_protobuf_wrappers_proto ),
359393 }
394+ for _ , override := range cfg .protoMappingOverrides {
395+ if dep , found := extraWellKnownTypesPerTypeName [override .TypeName ]; found {
396+ fdpList = append (fdpList , dep .fileDescriptor )
397+ }
398+ }
360399 fdpList = append (fdpList , cache .getFileDescriptorProtos ()... )
361400
362401 fds := & descriptorpb.FileDescriptorSet {
@@ -402,7 +441,7 @@ func messageDependsOnFile(msg protoreflect.MessageDescriptor, file protoreflect.
402441// For proto2, we propagate the mode->label annotation as expected.
403442//
404443// Messages are always nullable, and repeated fields are as well.
405- func tableFieldSchemaToFieldDescriptorProto (field * storagepb.TableFieldSchema , idx int32 , scope string , useProto3 bool ) * descriptorpb.FieldDescriptorProto {
444+ func tableFieldSchemaToFieldDescriptorProto (field * storagepb.TableFieldSchema , idx int32 , scope string , cfg * customConfig ) * descriptorpb.FieldDescriptorProto {
406445 name := field .GetName ()
407446 var fdp * descriptorpb.FieldDescriptorProto
408447
@@ -411,46 +450,35 @@ func tableFieldSchemaToFieldDescriptorProto(field *storagepb.TableFieldSchema, i
411450 Name : proto .String (name ),
412451 Number : proto .Int32 (idx ),
413452 TypeName : proto .String (scope ),
414- Label : convertModeToLabel (field .GetMode (), useProto3 ),
453+ Label : convertModeToLabel (field .GetMode (), cfg . useProto3 ),
415454 }
416455 } else if field .GetType () == storagepb .TableFieldSchema_RANGE {
417456 fdp = & descriptorpb.FieldDescriptorProto {
418457 Name : proto .String (name ),
419458 Number : proto .Int32 (idx ),
420459 TypeName : proto .String (fmt .Sprintf ("%s%s" , rangeTypesPrefix , strings .ToLower (field .GetRangeElementType ().GetType ().String ()))),
421- Label : convertModeToLabel (field .GetMode (), useProto3 ),
460+ Label : convertModeToLabel (field .GetMode (), cfg . useProto3 ),
422461 }
423462 } else {
424- // For (REQUIRED||REPEATED) fields for proto3, or all cases for proto2, we can use the expected scalar types.
425- if field .GetMode () != storagepb .TableFieldSchema_NULLABLE || ! useProto3 {
426- outType := bqTypeToFieldTypeMap [field .GetType ()]
427- fdp = & descriptorpb.FieldDescriptorProto {
428- Name : proto .String (name ),
429- Number : proto .Int32 (idx ),
430- Type : outType .Enum (),
431- Label : convertModeToLabel (field .GetMode (), useProto3 ),
432- }
463+ typeName , outType , label := resolveType (scope , field , cfg )
464+ fdp = & descriptorpb.FieldDescriptorProto {
465+ Name : proto .String (name ),
466+ Number : proto .Int32 (idx ),
467+ Type : outType .Enum (),
468+ TypeName : typeName ,
469+ Label : label ,
470+ }
433471
434- // Special case: proto2 repeated fields may benefit from using packed annotation.
435- if field .GetMode () == storagepb .TableFieldSchema_REPEATED && ! useProto3 {
436- for _ , v := range packedTypes {
437- if outType == v {
438- fdp .Options = & descriptorpb.FieldOptions {
439- Packed : proto .Bool (true ),
440- }
441- break
472+ // Special case: proto2 repeated fields may benefit from using packed annotation.
473+ if field .GetMode () == storagepb .TableFieldSchema_REPEATED && ! cfg .useProto3 {
474+ for _ , v := range packedTypes {
475+ if outType == v {
476+ fdp .Options = & descriptorpb.FieldOptions {
477+ Packed : proto .Bool (true ),
442478 }
479+ break
443480 }
444481 }
445- } else {
446- // For NULLABLE proto3 fields, use a wrapper type.
447- fdp = & descriptorpb.FieldDescriptorProto {
448- Name : proto .String (name ),
449- Number : proto .Int32 (idx ),
450- Type : descriptorpb .FieldDescriptorProto_TYPE_MESSAGE .Enum (),
451- TypeName : proto .String (bqTypeToWrapperMap [field .GetType ()]),
452- Label : descriptorpb .FieldDescriptorProto_LABEL_OPTIONAL .Enum (),
453- }
454482 }
455483 }
456484 if nameRequiresAnnotation (name ) {
@@ -468,6 +496,23 @@ func tableFieldSchemaToFieldDescriptorProto(field *storagepb.TableFieldSchema, i
468496 return fdp
469497}
470498
499+ func resolveType (scope string , field * storagepb.TableFieldSchema , cfg * customConfig ) (* string , descriptorpb.FieldDescriptorProto_Type , * descriptorpb.FieldDescriptorProto_Label ) {
500+ path := strings .TrimPrefix (strings .ReplaceAll (scope , "__" , "." ), "root." )
501+ if override := cfg .protoMappingOverrides .getByField (field , path ); override != nil {
502+ var typeName * string
503+ if override .TypeName != "" {
504+ typeName = proto .String (override .TypeName )
505+ }
506+ return typeName , override .Type , convertModeToLabel (field .GetMode (), cfg .useProto3 )
507+ }
508+ // For (REQUIRED||REPEATED) fields for proto3, or all cases for proto2, we can use the expected scalar types.
509+ if field .GetMode () != storagepb .TableFieldSchema_NULLABLE || ! cfg .useProto3 {
510+ return nil , bqTypeToFieldTypeMap [field .GetType ()], convertModeToLabel (field .GetMode (), cfg .useProto3 )
511+ }
512+ // For NULLABLE proto3 fields, use a wrapper type.
513+ return proto .String (bqTypeToWrapperMap [field .GetType ()]), descriptorpb .FieldDescriptorProto_TYPE_MESSAGE , descriptorpb .FieldDescriptorProto_LABEL_OPTIONAL .Enum ()
514+ }
515+
471516// nameRequiresAnnotation determines whether a field name requires unicode-annotation.
472517func nameRequiresAnnotation (in string ) bool {
473518 return ! protoreflect .Name (in ).IsValid ()
0 commit comments