From efa3553a859bb4b3211165889fefaf786db3a9d6 Mon Sep 17 00:00:00 2001 From: apstndb <803393+apstndb@users.noreply.github.com> Date: Sat, 27 Jun 2026 04:48:45 +0900 Subject: [PATCH 1/2] Add PROTO BUNDLE integration coverage and downstream usage docs. Downstream validation in spanner-emulator-survey confirmed the FileDescriptorSet bootstrap options work without API changes; document the pairing pattern and add emulator tests for both descriptor option variants. Co-authored-by: Cursor --- README.md | 19 ++++++++ example_test.go | 31 +++++++++++++ options.go | 2 +- proto_bundle_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 proto_bundle_test.go diff --git a/README.md b/README.md index 8f94281..f4d7232 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,25 @@ func TestFoo(t *testing.T) { } ``` +`CREATE PROTO BUNDLE` and `ALTER PROTO BUNDLE` setup DDLs need serialized +descriptors alongside the DDL text. Pair [WithSetupDDLs] with +[WithSetupFileDescriptorSet] or [WithSetupRawFileDescriptorSet]: + +```go +func TestProtoBundle(t *testing.T) { + fds := &descriptorpb.FileDescriptorSet{ /* ... */ } + + env := spanemuboost.SetupEmulatorWithClients(t, + spanemuboost.WithSetupDDLs([]string{ + "CREATE PROTO BUNDLE (`examples.shipping.Order`)", + }), + spanemuboost.WithSetupFileDescriptorSet(fds), + ) + + // env.Client is ready with the proto bundle applied +} +``` + For non-test usage (e.g. embedding the emulator in an application where the `testing` package is unavailable), see runnable examples on [pkg.go.dev](https://pkg.go.dev/github.com/apstndb/spanemuboost#pkg-examples). ### Shared runtime, database-per-case diff --git a/example_test.go b/example_test.go index e9325ef..cdf87ec 100644 --- a/example_test.go +++ b/example_test.go @@ -10,6 +10,8 @@ import ( "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "github.com/apstndb/spanemuboost" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" ) func ExampleRunEmulatorWithClients() { @@ -33,6 +35,35 @@ func ExampleRunEmulatorWithClients() { // Output: {fields: [type:{code:INT64}], values: [string_value:"1"]} } +func ExampleWithSetupFileDescriptorSet() { + ctx := context.Background() + + fds := &descriptorpb.FileDescriptorSet{ + File: []*descriptorpb.FileDescriptorProto{ + { + Package: proto.String("examples.shipping"), + MessageType: []*descriptorpb.DescriptorProto{ + {Name: proto.String("Order")}, + }, + }, + }, + } + + env, err := spanemuboost.RunEmulatorWithClients(ctx, + spanemuboost.WithSetupDDLs([]string{ + "CREATE PROTO BUNDLE (`examples.shipping.Order`)", + }), + spanemuboost.WithSetupFileDescriptorSet(fds), + ) + if err != nil { + log.Fatalln(err) + return + } + defer env.Close() //nolint:errcheck + + _ = env.Client +} + func ExampleOpenClients() { ctx := context.Background() diff --git a/options.go b/options.go index d88dd99..90e4b41 100644 --- a/options.go +++ b/options.go @@ -339,7 +339,7 @@ func WithSetupDDLs(ddls []string) Option { // WithSetupFileDescriptorSet sets proto descriptors for CREATE/ALTER PROTO BUNDLE // statements in [WithSetupDDLs]. Use this option together with setup DDLs that // reference proto bundles; the value is serialized for CreateDatabase and -// UpdateDatabaseDdl requests. +// UpdateDatabaseDdl requests at bootstrap time. // Calling this multiple times replaces the previous value. func WithSetupFileDescriptorSet(fds *descriptorpb.FileDescriptorSet) Option { var raw []byte diff --git a/proto_bundle_test.go b/proto_bundle_test.go new file mode 100644 index 0000000..d9dd93e --- /dev/null +++ b/proto_bundle_test.go @@ -0,0 +1,108 @@ +package spanemuboost + +import ( + "testing" + + "cloud.google.com/go/spanner" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +func TestSetupEmulatorWithClientsProtoBundle(t *testing.T) { + env := SetupEmulatorWithClients(t, + WithSetupDDLs([]string{"CREATE PROTO BUNDLE (`examples.shipping.Order`)"}), + WithSetupFileDescriptorSet(exampleShippingFileDescriptorSet()), + ) + + ctx := t.Context() + var rows []struct { + SchemaName string `spanner:"SCHEMA_NAME"` + ProtoBundle []byte `spanner:"PROTO_BUNDLE"` + } + err := spanner.SelectAll( + env.Client.Single().Query(ctx, spanner.Statement{ + SQL: "SELECT SCHEMA_NAME, PROTO_BUNDLE FROM INFORMATION_SCHEMA.SCHEMATA", + }), + &rows, + spanner.WithLenient(), + ) + if err != nil { + t.Fatal(err) + } + + var protoBundle []byte + for _, row := range rows { + if row.SchemaName == "" && len(row.ProtoBundle) > 0 { + protoBundle = row.ProtoBundle + break + } + } + if len(protoBundle) == 0 { + t.Fatal("SCHEMATA.PROTO_BUNDLE is empty for default schema") + } +} + +func TestSetupClientsProtoBundleWithRawDescriptors(t *testing.T) { + raw, err := proto.Marshal(exampleShippingFileDescriptorSet()) + if err != nil { + t.Fatalf("proto.Marshal: %v", err) + } + + emu := SetupEmulator(t, EnableInstanceAutoConfigOnly()) + clients := SetupClients(t, emu, + WithRandomDatabaseID(), + WithSetupDDLs([]string{"CREATE PROTO BUNDLE (`examples.shipping.Order`)"}), + WithSetupRawFileDescriptorSet(raw), + ) + + ctx := t.Context() + var rows []struct { + SchemaName string `spanner:"SCHEMA_NAME"` + ProtoBundle []byte `spanner:"PROTO_BUNDLE"` + } + err = spanner.SelectAll( + clients.Client.Single().Query(ctx, spanner.Statement{ + SQL: "SELECT SCHEMA_NAME, PROTO_BUNDLE FROM INFORMATION_SCHEMA.SCHEMATA", + }), + &rows, + spanner.WithLenient(), + ) + if err != nil { + t.Fatal(err) + } + + var protoBundle []byte + for _, row := range rows { + if row.SchemaName == "" && len(row.ProtoBundle) > 0 { + protoBundle = row.ProtoBundle + break + } + } + if len(protoBundle) == 0 { + t.Fatal("SCHEMATA.PROTO_BUNDLE is empty for default schema") + } +} + +func exampleShippingFileDescriptorSet() *descriptorpb.FileDescriptorSet { + return &descriptorpb.FileDescriptorSet{ + File: []*descriptorpb.FileDescriptorProto{ + { + Package: proto.String("examples.shipping"), + EnumType: []*descriptorpb.EnumDescriptorProto{ + {Name: proto.String("ShippingSpeed")}, + }, + MessageType: []*descriptorpb.DescriptorProto{ + { + Name: proto.String("Order"), + EnumType: []*descriptorpb.EnumDescriptorProto{ + {Name: proto.String("Status")}, + }, + NestedType: []*descriptorpb.DescriptorProto{ + {Name: proto.String("Address")}, + }, + }, + }, + }, + }, + } +} From 712605a41c767c85d468fd42ecd5a88cb538a293 Mon Sep 17 00:00:00 2001 From: apstndb <803393+apstndb@users.noreply.github.com> Date: Sat, 27 Jun 2026 05:07:53 +0900 Subject: [PATCH 2/2] Use valid proto3 FileDescriptorProto in PROTO BUNDLE tests. Set file name, syntax, and enum values so the bootstrap helper matches Cloud Spanner descriptor expectations. Co-authored-by: Cursor --- example_test.go | 2 ++ proto_bundle_test.go | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/example_test.go b/example_test.go index cdf87ec..537dfc8 100644 --- a/example_test.go +++ b/example_test.go @@ -41,6 +41,8 @@ func ExampleWithSetupFileDescriptorSet() { fds := &descriptorpb.FileDescriptorSet{ File: []*descriptorpb.FileDescriptorProto{ { + Name: proto.String("shipping.proto"), + Syntax: proto.String("proto3"), Package: proto.String("examples.shipping"), MessageType: []*descriptorpb.DescriptorProto{ {Name: proto.String("Order")}, diff --git a/proto_bundle_test.go b/proto_bundle_test.go index d9dd93e..27ad106 100644 --- a/proto_bundle_test.go +++ b/proto_bundle_test.go @@ -87,15 +87,27 @@ func exampleShippingFileDescriptorSet() *descriptorpb.FileDescriptorSet { return &descriptorpb.FileDescriptorSet{ File: []*descriptorpb.FileDescriptorProto{ { + Name: proto.String("shipping.proto"), + Syntax: proto.String("proto3"), Package: proto.String("examples.shipping"), EnumType: []*descriptorpb.EnumDescriptorProto{ - {Name: proto.String("ShippingSpeed")}, + { + Name: proto.String("ShippingSpeed"), + Value: []*descriptorpb.EnumValueDescriptorProto{ + {Name: proto.String("SHIPPING_SPEED_UNSPECIFIED"), Number: proto.Int32(0)}, + }, + }, }, MessageType: []*descriptorpb.DescriptorProto{ { Name: proto.String("Order"), EnumType: []*descriptorpb.EnumDescriptorProto{ - {Name: proto.String("Status")}, + { + Name: proto.String("Status"), + Value: []*descriptorpb.EnumValueDescriptorProto{ + {Name: proto.String("STATUS_UNSPECIFIED"), Number: proto.Int32(0)}, + }, + }, }, NestedType: []*descriptorpb.DescriptorProto{ {Name: proto.String("Address")},