Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

structs not supported #281

Closed
egonelbre opened this issue Aug 7, 2024 · 3 comments · Fixed by #282 or #289
Closed

structs not supported #281

egonelbre opened this issue Aug 7, 2024 · 3 comments · Fixed by #282 or #289
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@egonelbre
Copy link
Contributor

https://pkg.go.dev/cloud.google.com/go/spanner supports structs https://pkg.go.dev/cloud.google.com/go/spanner#hdr-Structs, however this feature is not exposed directly in go-sql-spanner.

For example the following code fails due to the check in

func checkIsValidType(v driver.Value) bool {

package main

import (
	"context"
	"database/sql"
	"fmt"

	_ "github.com/googleapis/go-sql-spanner"
	"github.com/googleapis/go-sql-spanner/examples"
)

func helloWorld(projectId, instanceId, databaseId string) error {
	ctx := context.Background()
	db, err := sql.Open("spanner", fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectId, instanceId, databaseId))
	if err != nil {
		return fmt.Errorf("failed to open database connection: %v\n", err)
	}
	defer db.Close()

	type Entry struct {
		ID   int64
		Name string
	}

	entries := []Entry{
		{ID: 0, Name: "Hello"},
		{ID: 1, Name: "World"},
	}

	rows, err := db.QueryContext(ctx, "SELECT id, name FROM UNNEST(@entries)", entries)
	if err != nil {
		return fmt.Errorf("failed to execute query: %v", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id int64
		var name string

		if err := rows.Scan(&id, &name); err != nil {
			return fmt.Errorf("failed to scan row values: %v", err)
		}
		fmt.Printf("%v %v\n", id, name)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("failed to execute query: %v", err)
	}
	return nil
}

func main() {
	examples.RunSampleOnEmulator(helloWorld)
}

If I change changeIsValidType to always return true, then the code above passes without problems.

There's probably a workaround for this, but I haven't, yet discovered it. Nevertheless, it would be nice to support struct and array of struct directly in go-sql-spanner.

@egonelbre egonelbre added priority: p3 Desirable enhancement or fix. May not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. labels Aug 7, 2024
egonelbre added a commit to egonelbre/go-sql-spanner that referenced this issue Aug 7, 2024
cloud.google.com/go/spanner supports passing structs as arguments to
queries.

Fixes googleapis#281
@egonelbre
Copy link
Contributor Author

egonelbre commented Aug 7, 2024

It's not pretty, but a workaround is to use GenericColumnValue and construct it manually, like:

// the entries can be replaced with:
spanner.GenericColumnValue{
	Type: &spannerpb.Type{
		Code: spannerpb.TypeCode_ARRAY,
		ArrayElementType: &spannerpb.Type{
			Code: spannerpb.TypeCode_STRUCT,
			StructType: &spannerpb.StructType{
				Fields: []*spannerpb.StructType_Field{
					{Name: "id", Type: &spannerpb.Type{Code: spannerpb.TypeCode_INT64}},
					{Name: "name", Type: &spannerpb.Type{Code: spannerpb.TypeCode_STRING}},
				},
			},
		},
	},
	Value: &structpb.Value{
		Kind: &structpb.Value_ListValue{
			ListValue: &structpb.ListValue{
				Values: []*structpb.Value{
					{
						Kind: &structpb.Value_ListValue{
							ListValue: &structpb.ListValue{
								Values: []*structpb.Value{
									{Kind: &structpb.Value_StringValue{StringValue: "0"}},
									{Kind: &structpb.Value_StringValue{StringValue: "Hello"}},
								},
							},
						},
					},
					{
						Kind: &structpb.Value_ListValue{
							ListValue: &structpb.ListValue{
								Values: []*structpb.Value{
									{Kind: &structpb.Value_StringValue{StringValue: "1"}},
									{Kind: &structpb.Value_StringValue{StringValue: "World"}},
								},
							},
						},
					},
				},
			},
		},
	},
}

Of course, the readability can be significantly improved via some helpers, e.g.

// the entries can be replaced with:
spanner.GenericColumnValue{
	Type: arrayOf(
		structOf(
			fieldOf("id", int64Type()),
			fieldOf("name", stringType()),
		),
	)
	...
}

func arrayOf(typ *spannerpb.Type) *spannerpb.Type {
	return &spannerpb.Type{
		Code: spannerpb.TypeCode_ARRAY,
		ArrayElementType: typ,
	}
}

func structOf(fields ...*spannerpb.StructType_Field) *spannerpb.Type {
	return &spannerpb.Type{
		Code: spannerpb.TypeCode_Struct,
		Fields: fields,
	}
}

func fieldOf(name string, typ *spannerpb.Type) *spannerpb.StructType_Field {
	return &spannerpb.StructType_Field{
		Name: name,
		Type: typ,
	}
}

func int64Type() *spannerpb.Type { return &spannerpb.Type{Kind: spannerpb.TypeCoode_INT64} }
func stringType() *spannerpb.Type { return &spannerpb.Type{Kind: spannerpb.TypeCoode_STRING} }

@newtonmunene99
Copy link

#282 seems to break gorm custom datatypes(https://gorm.io/docs/data_types.html). My custom datatype valuer and scanner interfaces just seem to be getting ignored. Downgrading to v1.6.0 seems to work again.

Not sure if I'm missing something but here's my current implementation

type ColumnMetadata struct {
	TableName  string              `gorm:"primaryKey"`
	ColumnName string              `gorm:"primaryKey"`
	Metadata   *Metadata `gorm:"type:bytes"`
	CreatedAt  time.Time           // Automatically managed by GORM for creation time
	UpdatedAt  time.Time           // Automatically managed by GORM for update time
}

type Metadata struct {}

// Scan scans value into Metadata and implements sql.Scanner interface
func (c *Metadata) Scan(value interface{}) error {
	b, ok := value.([]byte)
	if !ok {
		return errors.New("type assertion to []byte failed")
	}
	return json.Unmarshal(b, &c)
}

// Value returns value of Metadata struct and implements driver.Valuer interface
func (c Metadata) Value() (driver.Value, error) {
	return json.Marshal(c)
}

func (c Metadata) GormDataType() string {
	return "bytes"
}

egonelbre added a commit to egonelbre/go-sql-spanner that referenced this issue Aug 29, 2024
When a type had implemented custom Value func to convert the type,
then the conversion method was not being called for structs.

Fixes googleapis#281
@egonelbre
Copy link
Contributor Author

@newtonmunene99 ah didn't think about that interaction. Sent a patch #289

olavloite pushed a commit that referenced this issue Aug 29, 2024
When a type had implemented custom Value func to convert the type,
then the conversion method was not being called for structs.

Fixes #281
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
2 participants