Skip to content

spanner: ToStruct does not properly decode if target struct field is a pointer to a type implementing proto.Message. #10703

@jmbradshaw79

Description

@jmbradshaw79

Client

Spanner

Environment

go version go1.23.0 darwin/arm64

Code and Dependencies

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"spanner-bug/examplepb"

	"cloud.google.com/go/spanner"
	"google.golang.org/api/option"
	"google.golang.org/protobuf/proto"
)

func main() {
	if len(os.Args) < 3 {
		log.Fatal("Usage: spanner-bug project-id instance database")
	}
	project := os.Args[1]
	instance := os.Args[2]
	database := os.Args[3]

	dsn := fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database)
	ctx := context.Background()
	spannerClient, err := spanner.NewClient(ctx, dsn, option.WithGRPCConnectionPool(4))
	if err != nil {
		log.Fatalf("Failed to create client %v", err)
	}
	_, err = spannerClient.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		exampleFieldInstance := &examplepb.ExampleProtoField{
			Field1: "test1",
			Field2: "test2",
		}
		exampleFieldInstanceProto, err := proto.Marshal(exampleFieldInstance)
		if err != nil {
			log.Fatalf("Failed to marshal examplepb.ExampleProtoField: %v", err)
		}
		stmt := spanner.NewStatement(`INSERT ExampleTable (ExampleId, ExampleProtoField) VALUES (@ExampleId, @ExampleProtoField)`)
		stmt.Params["ExampleId"] = "id-123"
		stmt.Params["ExampleProtoField"] = exampleFieldInstanceProto
		_, err = txn.Update(ctx, stmt)
		return err
	})
	if err != nil {
		log.Fatalf("Failed to insert data: %v", err)
	}
	stmt := spanner.NewStatement(`SELECT ExampleId, ExampleProtoField FROM ExampleTable where ExampleId = @exampleId LIMIT 1`)
	stmt.Params["ExampleId"] = "id-123"

	iter := spannerClient.Single().Query(ctx, stmt)
	row, err := iter.Next()
	if err != nil {
		log.Fatalf("failed to get example row from iterator: %v", err)
	}
	var e examplepb.ExampleTable
	err = row.ToStructLenient(&e)
	if err != nil {
		log.Fatalf("failed to call ToStructLenient on example row: %v", err)
	}
}
syntax = "proto3";
package examplepb;
option go_package = "/;examplepb";

message ExampleTable {
  string exampleId = 1;
  ExampleProtoField exampleProtoField = 2;
}

message ExampleProtoField {
  string field1 = 1;
  string field2 = 2;
}
CREATE PROTO BUNDLE (
examplepb.ExampleTable,
examplepb.ExampleProtoField,
);
CREATE TABLE ExampleTable (
  ExampleId STRING(16),
  ExampleProtoField examplepb.ExampleProtoField,
) PRIMARY KEY (ExampleId);
protoc --include_imports --go_out=examplepb --descriptor_set_out=types.pb examplepb/*.proto
gcloud spanner databases ddl update ${DATABASE} --instance=${INSTANCE} --ddl-file=database/proto-bundle.sql --proto-descriptors-file=database/types.pb
gcloud spanner databases ddl update ${DATABASE} --instance=${INSTANCE} --ddl-file=database/schema.sql 
go.mod
module spanner-bug

go 1.23.0

require (
	cloud.google.com/go/spanner v1.67.0
	google.golang.org/api v0.191.0
	google.golang.org/protobuf v1.34.2
)

require (
	cel.dev/expr v0.15.0 // indirect
	cloud.google.com/go v0.115.0 // indirect
	cloud.google.com/go/auth v0.8.0 // indirect
	cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
	cloud.google.com/go/compute/metadata v0.5.0 // indirect
	github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect
	github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
	github.com/envoyproxy/go-control-plane v0.12.0 // indirect
	github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/go-logr/logr v1.4.2 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
	github.com/golang/protobuf v1.5.4 // indirect
	github.com/google/s2a-go v0.1.8 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
	github.com/googleapis/gax-go/v2 v2.13.0 // indirect
	go.opencensus.io v0.24.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
	go.opentelemetry.io/otel v1.24.0 // indirect
	go.opentelemetry.io/otel/metric v1.24.0 // indirect
	go.opentelemetry.io/otel/trace v1.24.0 // indirect
	golang.org/x/crypto v0.25.0 // indirect
	golang.org/x/net v0.27.0 // indirect
	golang.org/x/oauth2 v0.22.0 // indirect
	golang.org/x/sync v0.8.0 // indirect
	golang.org/x/sys v0.22.0 // indirect
	golang.org/x/text v0.16.0 // indirect
	golang.org/x/time v0.6.0 // indirect
	golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
	google.golang.org/grpc v1.64.1 // indirect
)

Expected behavior

row.ToStructLenient() and/or ToStruct() executes successfully

Actual behavior

Call errors with **examplepb.ExampleProtoField cannot be used for decoding PROTO

2024/08/17 13:45:06 failed to call ToStructLenient on example row: spanner: code = "InvalidArgument", desc = "cannot decode field ExampleProtoField of Cloud Spanner STRUCT fields:{name:\"ExampleId\" type:{code:STRING}} fields:{name:\"ExampleProtoField\" type:{code:PROTO proto_type_fqn:\"examplepb.ExampleProtoField\"}}, type **examplepb.ExampleProtoField cannot be used for decoding PROTO"

Screenshots

Additional context

Portion of example.pb.go

...
type ExampleTable struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	ExampleId         string             `protobuf:"bytes,1,opt,name=exampleId,proto3" json:"exampleId,omitempty"`
	ExampleProtoField *ExampleProtoField `protobuf:"bytes,2,opt,name=exampleProtoField,proto3" json:"exampleProtoField,omitempty"`
}
...

Works fine if the fields defined within the struct are not pointers to proto message types. Like so,

type ExampleTableNoPtr struct {
	ExampleId         string 
	ExampleProtoField ExampleProtoField 
}

Metadata

Metadata

Assignees

Labels

api: spannerIssues related to the Spanner API.triage meI really want to be triaged.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions