package repo import ( "context" "fmt" "cloud.google.com/go/spanner" "example.com/bridging-sample/api/internal/models" "github.com/google/uuid" "google.golang.org/api/iterator" ) type ItemRepo struct { client *spanner.Client } func NewItemRepo(c *spanner.Client) *ItemRepo { return &ItemRepo{client: c} } // Create with UUID PK (fits the DDL Option B we discussed) func (r *ItemRepo) Create(ctx context.Context, m models.Item) (string, error) { id := uuid.NewString() stmt := spanner.Statement{ SQL: `INSERT INTO item_stock (id, name, amount, price, sales_amount) VALUES (@id, @name, @amount, CAST(@price AS NUMERIC), @sales_amount)`, Params: map[string]interface{}{ "id": id, "name": m.Name, "amount": m.Amount, "price": m.Price, "sales_amount": m.SalesAmount, }, } _, err := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { return tx.BufferWrite([]*spanner.Mutation{ spanner.InsertOrUpdateMap("item_stock", map[string]interface{}{ "id": id, "name": m.Name, "amount": m.Amount, "price": m.Price, "sales_amount": m.SalesAmount, }), }) }) if err != nil { // fallback to statement example: _, altErr := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { _, e := tx.Update(ctx, stmt) return e }) if altErr != nil { return "", fmt.Errorf("create item: %w", err) } } return id, nil } func (r *ItemRepo) Get(ctx context.Context, id string) (*models.Item, error) { stmt := spanner.Statement{ SQL: `SELECT id, name, amount, CAST(price AS STRING) AS price, sales_amount FROM item_stock WHERE id = @id`, Params: map[string]interface{}{"id": id}, } iter := r.client.Single().Query(ctx, stmt) defer iter.Stop() row, err := iter.Next() if err != nil { return nil, err } var m models.Item if err := row.Columns(&m.Id, &m.Name, &m.Amount, &m.Price, &m.SalesAmount); err != nil { return nil, err } return &m, nil } func (r *ItemRepo) List(ctx context.Context, limit int32) ([]models.Item, error) { if limit <= 0 { limit = 50 } stmt := spanner.Statement{ SQL: fmt.Sprintf(`SELECT id, name, amount, CAST(price AS STRING) AS price, sales_amount FROM item_stock ORDER BY id DESC LIMIT %d`, limit), } it := r.client.Single().Query(ctx, stmt) defer it.Stop() var out []models.Item for { row, err := it.Next() if err == iterator.Done { break } if err != nil { return nil, err } var m models.Item if err := row.Columns(&m.Id, &m.Name, &m.Amount, &m.Price, &m.SalesAmount); err != nil { return nil, err } out = append(out, m) } return out, nil } func (r *ItemRepo) Delete(ctx context.Context, id string) error { mut := spanner.Delete("item_stock", spanner.Key{id}) _, err := r.client.Apply(ctx, []*spanner.Mutation{mut}) return err } // Example of running raw SQL (useful in emulator): func (r *ItemRepo) ExecSQL(ctx context.Context, sql string) error { _, err := r.client.ReadWriteTransaction(ctx, func(ctx context.Context, tx *spanner.ReadWriteTransaction) error { _, e := tx.Update(ctx, spanner.Statement{SQL: sql}) return e }) return err }