From e502c7cc408f5b0f79b905c21cbd2e9785468f46 Mon Sep 17 00:00:00 2001 From: Nikita Karmatskikh Date: Fri, 26 Nov 2021 01:42:25 +0300 Subject: [PATCH] transformer: add transformer for unsetting empty values --- example_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ transformer.go | 17 ++++++++++++ 2 files changed, 89 insertions(+) diff --git a/example_test.go b/example_test.go index 341d86f..fd37db0 100644 --- a/example_test.go +++ b/example_test.go @@ -19,6 +19,7 @@ import ( "github.com/scylladb/gocqlx/v2/qb" "github.com/scylladb/gocqlx/v2/table" "golang.org/x/sync/errgroup" + "gopkg.in/inf.v0" ) // Running examples locally: @@ -47,6 +48,7 @@ func TestExample(t *testing.T) { pagingEfficientFullTableScan(t, session) lwtLock(t, session) + unsetEmptyValues(t, session) } // This example shows how to use query builders and table models to build @@ -705,6 +707,76 @@ func lwtLock(t *testing.T, session gocqlx.Session) { } } +// This example shows how to reuse the same insert statement with +// partially filled parameters without generating tombstones for empty columns. +func unsetEmptyValues(t *testing.T, session gocqlx.Session) { + err := session.ExecStmt(`CREATE KEYSPACE IF NOT EXISTS examples WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}`) + if err != nil { + t.Fatal("create keyspace:", err) + } + + type Operation struct { + ID string + ClientID string + Type string + PaymentID string + Fee *inf.Dec + } + err = session.ExecStmt(`CREATE TABLE IF NOT EXISTS examples.operations ( + id text PRIMARY KEY, + client_id text, + type text, + payment_id text, + fee decimal)`) + if err != nil { + t.Fatal("create table:", err) + } + + insertOperation := qb.Insert("examples.operations"). + Columns("id", "client_id", "type", "payment_id", "fee") + + // Insert operation with empty paymentID. + insertQuery := insertOperation.Query(session). + WithBindTransformer(gocqlx.UnsetEmptyTransformer). + BindStruct(Operation{ + ID: "1", + ClientID: "42", + Type: "Transfer", + Fee: inf.NewDec(1, 1), + }) + if err := insertQuery.ExecRelease(); err != nil { + t.Fatal("ExecRelease() failed:", err) + } + + // Set default transformer to avoid setting it for each query. + gocqlx.DefaultBindTransformer = gocqlx.UnsetEmptyTransformer + defer func() { + gocqlx.DefaultBindTransformer = nil + }() + + // Insert operation with empty fee. + insertQuery = insertOperation.Query(session). + BindStruct(Operation{ + ID: "2", + ClientID: "42", + Type: "Input", + PaymentID: "1", + }) + if err := insertQuery.ExecRelease(); err != nil { + t.Fatal("ExecRelease() failed:", err) + } + + // Query and displays data. + var ops []*Operation + if err := qb.Select("examples.operations").Query(session).Select(&ops); err != nil { + t.Fatal("Select() failed:", err) + } + + for _, op := range ops { + t.Logf("%+v", *op) + } +} + func mustParseUUID(s string) gocql.UUID { u, err := gocql.ParseUUID(s) if err != nil { diff --git a/transformer.go b/transformer.go index bb90d0f..3c80f6e 100644 --- a/transformer.go +++ b/transformer.go @@ -4,6 +4,12 @@ package gocqlx +import ( + "reflect" + + "github.com/gocql/gocql" +) + // Transformer transforms the value of the named parameter to another value. type Transformer func(name string, val interface{}) interface{} @@ -11,3 +17,14 @@ type Transformer func(name string, val interface{}) interface{} // // A custom transformer can always be set per Query. var DefaultBindTransformer Transformer + +// UnsetEmptyTransformer unsets all empty parameters. +// It helps to avoid tombstones when using the same insert/update +// statement for filled and partially filled named parameters. +var UnsetEmptyTransformer = func(name string, val interface{}) interface{} { + v := reflect.ValueOf(val) + if v.IsZero() { + return gocql.UnsetValue + } + return val +}