From 268431100ccb9344842765c134f52a4bbe9efeea Mon Sep 17 00:00:00 2001 From: Pinglei Guo Date: Sun, 10 Mar 2019 15:09:53 -0700 Subject: [PATCH 1/3] [runtime] Support if in delete statement #13 --- CHANGELOG.md | 4 ++ README.md | 1 + cqlc/cqlc.go | 103 +++++++++++++++++++++------------------------- cqlc/cqlc_test.go | 10 +++++ cqlc/render.go | 16 ++++++- 5 files changed, 76 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7649ae..722cceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ NOTE: this file format is based on [gaocegege/maintainer](https://github.com/gao ## Unreleased +0.12.2 + +- support `IF` statement in `DELETE` + ## 0.12.1 (2019-02-18) - previous release didn't update all the mapping in generator diff --git a/README.md b/README.md index 82436cd..d25fbb3 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ it is mainly for generating runtime code that ships with the library The main modification to the runtime are listed below - [support update map value by key](doc/set-map-value-by-key.md), previously, cqlc can only update entire map. (This change only requires update runtime) +- support `IF` in `DELETE` ### Generator diff --git a/cqlc/cqlc.go b/cqlc/cqlc.go index 9db2154..5c3f82a 100644 --- a/cqlc/cqlc.go +++ b/cqlc/cqlc.go @@ -80,12 +80,15 @@ type ReadOptions struct { // Context represents the state of the CQL statement that is being built by the application. type Context struct { - Operation OperationType - Table Table - Columns []Column - Bindings []ColumnBinding - CASBindings []ColumnBinding - Conditions []Condition + Operation OperationType + Table Table + Columns []Column + Bindings []ColumnBinding + CASBindings []ColumnBinding + // Conditions is used by WHERE to locate primary key + Conditions []Condition + // IfConditions is used by IF which is put after WHERE to restrict non primary key columns + IfConditions []Condition ResultBindings map[string]ColumnBinding // Debug flag will cause all CQL statements to get logged Debug bool @@ -146,6 +149,11 @@ type UniqueFetchable interface { FetchOne(*gocql.Session) (bool, error) } +type IfQueryStep interface { + If(conditions ...Condition) Query + Query +} + type Query interface { Executable Fetchable @@ -155,7 +163,7 @@ type Query interface { type SelectWhereStep interface { Fetchable - Where(conditions ...Condition) Query + Where(conditions ...Condition) IfQueryStep } type SelectFromStep interface { @@ -370,14 +378,19 @@ func (c *Context) Apply(cols ...ColumnBinding) SetValueStep { return c } -// Adds column bindings whose values will nbe populated if a CAS operation +// Adds column bindings whose values will be populated if a CAS operation // is applied. func (c *Context) IfExists(cols ...ColumnBinding) CompareAndSwap { c.CASBindings = cols return c } -func (c *Context) Where(cond ...Condition) Query { +func (c *Context) If(cond ...Condition) Query { + c.IfConditions = cond + return c +} + +func (c *Context) Where(cond ...Condition) IfQueryStep { c.Conditions = cond return c } @@ -579,60 +592,37 @@ func BuildStatement(c *Context) (stmt string, placeHolders []interface{}, err er } } - for _, cond := range c.Conditions { - v := cond.Binding.Value - switch reflect.TypeOf(v).Kind() { - case reflect.Slice: - s := reflect.ValueOf(v) - for i := 0; i < s.Len(); i++ { - placeHolders = append(placeHolders, s.Index(i).Interface()) - } - case reflect.Array: + bindCondition := func(conditions []Condition) error { + for _, cond := range conditions { + v := cond.Binding.Value + switch reflect.TypeOf(v).Kind() { + case reflect.Slice: + s := reflect.ValueOf(v) + for i := 0; i < s.Len(); i++ { + placeHolders = append(placeHolders, s.Index(i).Interface()) + } + case reflect.Array: - // Not really happy about having to special case UUIDs - // but this works for now + // Not really happy about having to special case UUIDs + // but this works for now - if val, ok := v.(gocql.UUID); ok { - placeHolders = append(placeHolders, val.Bytes()) - } else { - return "", nil, bindingErrorf("Cannot bind component: %+v (type: %s)", v, reflect.TypeOf(v)) + if val, ok := v.(gocql.UUID); ok { + placeHolders = append(placeHolders, val.Bytes()) + } else { + return bindingErrorf("Cannot bind component: %+v (type: %s)", v, reflect.TypeOf(v)) + } + default: + placeHolders = append(placeHolders, v) } - default: - placeHolders = append(placeHolders, v) } + return nil } - c.Dispose() - - return stmt, placeHolders, nil -} - -// Deprecated -// NOTE: (pingginp) this is used by Exec and unlike Prepare, it didn't handle expand binding -func BuildStatementOld(c *Context) (stmt string, placeHolders []interface{}, err error) { - // TODO Does this function need to get exported? - stmt, err = c.RenderCQL() - if err != nil { - return stmt, nil, err + if err := bindCondition(c.Conditions); err != nil { + return "", nil, err } - - bindings := len(c.Bindings) // TODO check whether this is nil - conditions := 0 - - if c.Conditions != nil { - conditions = len(c.Conditions) - } - - placeHolders = make([]interface{}, bindings+conditions) - - for i, bind := range c.Bindings { - placeHolders[i] = bind.Value - } - - if c.Conditions != nil { - for i, cond := range c.Conditions { - placeHolders[i+bindings] = cond.Binding.Value - } + if err := bindCondition(c.IfConditions); err != nil { + return "", nil, err } c.Dispose() @@ -681,6 +671,7 @@ func (c *Context) Dispose() { c.Table = nil c.Bindings = nil c.Conditions = nil + c.IfConditions = nil c.CASBindings = nil } diff --git a/cqlc/cqlc_test.go b/cqlc/cqlc_test.go index 0490d4f..0343b4a 100644 --- a/cqlc/cqlc_test.go +++ b/cqlc/cqlc_test.go @@ -317,6 +317,16 @@ func (s *CqlTestSuite) TestDeleteRow() { assert.Equal(s.T(), cql, "DELETE FROM foo WHERE id = ?") } +func (s *CqlTestSuite) TestDeleteRowIf() { + idCol := &MockAsciiColumn{name: "id"} + ageCol := &MockInt32Column{name: "age"} + c := NewContext() + c.Delete().From(s.table).Where(idCol.Eq("x")).If(ageCol.Eq(28)) + cql, err := c.RenderCQL() + assert.NoError(s.T(), err) + assert.Equal(s.T(), cql, "DELETE FROM foo WHERE id = ? IF age = ?") +} + func (s *CqlTestSuite) TestDeleteColumn() { idCol := &MockAsciiColumn{name: "id"} barCol := &MockAsciiColumn{name: "bar"} diff --git a/cqlc/render.go b/cqlc/render.go index a65ee86..6d86b1d 100644 --- a/cqlc/render.go +++ b/cqlc/render.go @@ -184,13 +184,25 @@ func renderDelete(ctx *Context, buf *bytes.Buffer) { } renderWhereClause(ctx, buf) + + if len(ctx.IfConditions) > 0 { + renderIfClause(ctx, buf) + } } func renderWhereClause(ctx *Context, buf *bytes.Buffer) { fmt.Fprint(buf, "WHERE ") + renderCondition(buf, ctx.Conditions) +} + +func renderIfClause(ctx *Context, buf *bytes.Buffer) { + fmt.Fprint(buf, " IF ") + renderCondition(buf, ctx.IfConditions) +} - whereFragments := make([]string, len(ctx.Conditions)) - for i, condition := range ctx.Conditions { +func renderCondition(buf *bytes.Buffer, conditions []Condition) { + whereFragments := make([]string, len(conditions)) + for i, condition := range conditions { col := condition.Binding.Column.ColumnName() pred := condition.Predicate From bb266353829ea5261e4965b20bdbc96d5d4b4a23 Mon Sep 17 00:00:00 2001 From: Pinglei Guo Date: Sun, 10 Mar 2019 16:15:33 -0700 Subject: [PATCH 2/3] [gen] Allow Eq on all columns for DELTE IF #13 - previously only primary key and those with index support it --- Makefile | 2 +- generator/template.go | 5 ++++- generator/tmpl.go | 11 +++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 38fcff7..a4f5378 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION = 0.12.1 +VERSION = 0.13.0 LDFLAGS = -X main.Version=$(VERSION) GO = CGO_ENABLED=0 go GO_LINUX = GOOS=linux GOARCH=amd64 $(GO) diff --git a/generator/template.go b/generator/template.go index 70c62b0..2477313 100644 --- a/generator/template.go +++ b/generator/template.go @@ -94,7 +94,10 @@ func columnType(c gocql.ColumnMetadata, table *gocql.TableMetadata) string { replacement = ".LastPartitioned" } baseType = strings.Replace(baseType, ".", replacement, 1) - } else if c.Index.Name != "" { + //} else if c.Index.Name != "" { + } else { + // NOTE: this is changed to allow Eq on all columns to use DELETE ... WHERE ... IF ... + // see https://github.com/pingginp/cqlc/issues/13 replacement := ".Equality" baseType = strings.Replace(baseType, ".", replacement, 1) } diff --git a/generator/tmpl.go b/generator/tmpl.go index 0757198..9c24784 100644 --- a/generator/tmpl.go +++ b/generator/tmpl.go @@ -81,8 +81,7 @@ const ( return cqlc.Condition{Binding: binding, Predicate: cqlc.InPredicate} } {{ end }} - {{ end }} - {{ if supportsClustering $col }} + {{ else if supportsClustering $col }} func (b * {{$QualifiedColStructType}}Column ) ClusterWith() string { return b.ColumnName() @@ -131,6 +130,14 @@ const ( binding := cqlc.ColumnBinding{Column: column, Value: value} return cqlc.Condition{Binding: binding, Predicate: cqlc.LePredicate} } + {{ else }} +// Eq is used in DELETE IF statement to filter on non primary key columns, +// introduced in https://github.com/pingginp/cqlc/issues/13 + func (b * {{$QualifiedColStructType}}Column ) Eq(value {{valueType $col}}) cqlc.Condition { + column := &{{$QualifiedColStructType}}Column{} + binding := cqlc.ColumnBinding{Column: column, Value: value} + return cqlc.Condition{Binding: binding, Predicate: cqlc.EqPredicate} + } {{ end }} {{ end }} From 1910b32eaa80662e5147dc8bf743bfb65d0f5a55 Mon Sep 17 00:00:00 2001 From: Pinglei Guo Date: Sun, 10 Mar 2019 16:28:09 -0700 Subject: [PATCH 3/3] [doc] Update change log --- CHANGELOG.md | 8 ++++++-- README.md | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722cceb..9532256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,13 @@ NOTE: this file format is based on [gaocegege/maintainer](https://github.com/gao ## Unreleased -0.12.2 +## 0.13.0 (2019-03-10) -- support `IF` statement in `DELETE` +Was going to make it 0.12.2 but since it breaks both runtime and generated code, bump minor version number + +- support `IF` in `DELETE` [#13](https://github.com/pingginp/cqlc/issues/13) +- in generated column bindings allow `Eq` on all columns, previously only primary key, index are allowed, which blocks using +other columns in condition queries after `If` ## 0.12.1 (2019-02-18) diff --git a/README.md b/README.md index d25fbb3..7db8068 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ it is mainly for generating runtime code that ships with the library The main modification to the runtime are listed below - [support update map value by key](doc/set-map-value-by-key.md), previously, cqlc can only update entire map. (This change only requires update runtime) -- support `IF` in `DELETE` +- support `IF` in `DELETE` [#13](https://github.com/pingginp/cqlc/issues/13) ### Generator @@ -62,6 +62,7 @@ The main modification to the generator are listed below - generator now compiles, caused by breaking change of constant name in gocql - support Cassandra 3 by adding a new literalType mapping for text -> string [#12](https://github.com/pingginp/cqlc/pull/12) +- allow `Eq` on all columns to support `IF` in `DELETE` The overall generator logic is