Add CQL support (#1)

* Add CQL flavor.

* Add support for CQL argument compilation.

* Adopt substests for interpolation tests.

* Add support for CQL interpolation.

* Add support for CQL blobs.

https://docs.datastax.com/en/cql-oss/3.x/cql/cql_reference/blob_r.html

* Add CQL timestamp interpolation support.

https://docs.datastax.com/en/cql-oss/3.x/cql/cql_reference/timestamp_type_r.html

* Add builder test for CQL.

* Add support for the NOW function.

https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/timeuuid_functions_r.html?hl=now

* Add support for CQL update IF statement.

https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/cqlUpdate.html#Conditionallyupdatingcolumns

* Drop table prefixes for CQL SELECTs.

* Switch to single quoting for CQL queries.

https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/escape_char_r.html

* Update documentation.

* Use valid CQL in tests.

* Support CQL LIMIT.
This commit is contained in:
Spencer Connaughton
2022-09-08 14:39:35 -04:00
committed by GitHub
parent 68495078ea
commit 0f54cd520c
16 changed files with 206 additions and 32 deletions
+2 -2
View File
@@ -110,7 +110,7 @@ Following are some utility methods to deal with special cases.
To learn how to use builders, check out [examples on GoDoc](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#pkg-examples).
### Build SQL for MySQL, PostgreSQL, SQLServer or SQLite
### Build SQL for MySQL, PostgreSQL, SQLServer, SQLite, or CQL
Parameter markers are different in MySQL, PostgreSQL, SQLServer and SQLite. This package provides some methods to set the type of markers (we call it "flavor") in all builders.
@@ -122,7 +122,7 @@ We can wrap any `Builder` with a default flavor through `WithFlavor`.
To be more verbose, we can use `PostgreSQL.NewSelectBuilder()` to create a `SelectBuilder` with the `PostgreSQL` flavor. All builders can be created in this way.
Right now, there are only three flavors, `MySQL`, `PostgreSQL`, `SQLServer` and `SQLite`. Open new issue to me to ask for a new flavor if you find it necessary.
Right now, there are five flavors, `MySQL`, `PostgreSQL`, `SQLServer`, `SQLite`, and `CQL`. Open new issue to me to ask for a new flavor if you find it necessary.
### Using `Struct` as a light weight ORM
+1 -1
View File
@@ -233,7 +233,7 @@ func (args *Args) compileArg(buf *bytes.Buffer, flavor Flavor, values []interfac
}
default:
switch flavor {
case MySQL, SQLite:
case MySQL, SQLite, CQL:
buf.WriteRune('?')
case PostgreSQL:
fmt.Fprintf(buf, "$%d", len(values)+1)
+15
View File
@@ -77,6 +77,21 @@ func TestArgs(t *testing.T) {
a.Equal(actual, expected)
}
DefaultFlavor = CQL
for expected, c := range cases {
args := new(Args)
for i := 1; i < len(c); i++ {
args.Add(c[i])
}
sql, values := args.Compile(c[0].(string))
actual := fmt.Sprintf("%v\n%v", sql, values)
a.Equal(actual, expected)
}
}
func toPostgreSQL(sql string) string {
+21
View File
@@ -103,3 +103,24 @@ func TestBuildWithPostgreSQL(t *testing.T) {
a.Equal(sql, "SELECT $1 AS col5 LEFT JOIN SELECT col1, col2 FROM t1 WHERE id = $2 AND level > $3 LEFT JOIN SELECT col3, col4 FROM t2 WHERE id = $4 AND level <= $5")
a.Equal(args, []interface{}{7890, 1234, 2, 4567, 5})
}
func TestBuildWithCQL(t *testing.T) {
a := assert.New(t)
ib1 := CQL.NewInsertBuilder()
ib1.InsertInto("t1").Cols("col1", "col2").Values(1, 2)
ib2 := CQL.NewInsertBuilder()
ib2.InsertInto("t2").Cols("col3", "col4").Values(3, 4)
old := DefaultFlavor
DefaultFlavor = CQL
defer func() {
DefaultFlavor = old
}()
sql, args := Build("BEGIN BATCH USING TIMESTAMP $0 $1; $2; APPLY BATCH;", 1481124356754405, ib1, ib2).Build()
a.Equal(sql, "BEGIN BATCH USING TIMESTAMP ? INSERT INTO t1 (col1, col2) VALUES (?, ?); INSERT INTO t2 (col3, col4) VALUES (?, ?); APPLY BATCH;")
a.Equal(args, []interface{}{1481124356754405, 1, 2, 3, 4})
}
+7
View File
@@ -16,6 +16,7 @@ const (
PostgreSQL
SQLite
SQLServer
CQL
)
var (
@@ -49,6 +50,8 @@ func (f Flavor) String() string {
return "SQLite"
case SQLServer:
return "SQLServer"
case CQL:
return "CQL"
}
return "<invalid>"
@@ -69,6 +72,8 @@ func (f Flavor) Interpolate(sql string, args []interface{}) (string, error) {
return sqliteInterpolate(sql, args...)
case SQLServer:
return sqlserverInterpolate(sql, args...)
case CQL:
return cqlInterpolate(sql, args...)
}
return "", ErrInterpolateNotImplemented
@@ -127,6 +132,8 @@ func (f Flavor) Quote(name string) string {
return fmt.Sprintf("`%s`", name)
case PostgreSQL, SQLServer, SQLite:
return fmt.Sprintf(`"%s"`, name)
case CQL:
return fmt.Sprintf("'%s'", name)
}
return name
+17
View File
@@ -121,3 +121,20 @@ func ExampleFlavor_Interpolate_sqlServer() {
// SELECT name FROM user WHERE id <> 1234 AND name = N'Charmy Liu' AND desc LIKE N'%mother\'s day%'
// <nil>
}
func ExampleFlavor_Interpolate_cql() {
sb := CQL.NewSelectBuilder()
sb.Select("name").From("user").Where(
sb.E("id", 1234),
sb.E("name", "Charmy Liu"),
)
sql, args := sb.Build()
query, err := CQL.Interpolate(sql, args)
fmt.Println(query)
fmt.Println(err)
// Output:
// SELECT name FROM user WHERE id = 1234 AND name = 'Charmy Liu'
// <nil>
}
+12 -12
View File
@@ -52,48 +52,48 @@ func ExampleReplaceInto() {
func ExampleInsertBuilder() {
ib := NewInsertBuilder()
ib.InsertInto("demo.user")
ib.Cols("id", "name", "status", "created_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
ib.Values(2, "Charmy Liu", 1, 1234567890)
ib.Cols("id", "name", "status", "created_at", "updated_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
sql, args := ib.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// INSERT INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
// INSERT INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
}
func ExampleInsertBuilder_insertIgnore() {
ib := NewInsertBuilder()
ib.InsertIgnoreInto("demo.user")
ib.Cols("id", "name", "status", "created_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
ib.Values(2, "Charmy Liu", 1, 1234567890)
ib.Cols("id", "name", "status", "created_at", "updated_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
sql, args := ib.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// INSERT IGNORE INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
// INSERT IGNORE INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
}
func ExampleInsertBuilder_replaceInto() {
ib := NewInsertBuilder()
ib.ReplaceInto("demo.user")
ib.Cols("id", "name", "status", "created_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
ib.Values(2, "Charmy Liu", 1, 1234567890)
ib.Cols("id", "name", "status", "created_at", "updated_at")
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
sql, args := ib.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// REPLACE INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
// REPLACE INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
}
+14 -2
View File
@@ -384,6 +384,11 @@ func sqliteInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(SQLite, query, args...)
}
// cqlInterpolate works the same as MySQL interpolating.
func cqlInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(CQL, query, args...)
}
func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
switch v := arg.(type) {
case nil:
@@ -453,7 +458,7 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
buf = appendHex(buf, v)
buf = append(buf, '\'')
case SQLServer:
case SQLServer, CQL:
buf = append(buf, "0x"...)
buf = appendHex(buf, v)
}
@@ -484,6 +489,9 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
case SQLServer:
buf = append(buf, v.Format("2006-01-02 15:04:05.999999 Z07:00")...)
case CQL:
buf = append(buf, v.Format("2006-01-02 15:04:05.999999Z0700")...)
}
buf = append(buf, '\'')
@@ -540,7 +548,11 @@ func quoteStringValue(buf []byte, s string, flavor Flavor) []byte {
buf = append(buf, "\\Z"...)
case '\'':
buf = append(buf, "\\'"...)
if flavor == CQL {
buf = append(buf, "''"...)
} else {
buf = append(buf, "\\'"...)
}
case '"':
buf = append(buf, "\\\""...)
+39 -5
View File
@@ -1,6 +1,7 @@
package sqlbuilder
import (
"fmt"
"strconv"
"testing"
"time"
@@ -9,7 +10,6 @@ import (
)
func TestFlavorInterpolate(t *testing.T) {
a := assert.New(t)
dt := time.Date(2019, 4, 24, 12, 23, 34, 123456789, time.FixedZone("CST", 8*60*60)) // 2019-04-24 12:23:34.987654321 CST
_, errOutOfRange := strconv.ParseInt("12345678901234567890", 10, 32)
cases := []struct {
@@ -137,13 +137,47 @@ func TestFlavorInterpolate(t *testing.T) {
"SELECT @p1", nil,
"", ErrInterpolateMissingArgs,
},
{
CQL,
"SELECT * FROM a WHERE name = ? AND state IN (?, ?, ?, ?, ?)", []interface{}{"I'm fine", 42, int8(8), int16(-16), int32(32), int64(64)},
"SELECT * FROM a WHERE name = 'I''m fine' AND state IN (42, 8, -16, 32, 64)", nil,
},
{
CQL,
"SELECT * FROM `a?` WHERE name = \"?\" AND state IN (?, '?', ?, ?, ?, ?, ?)", []interface{}{"\r\n\b\t\x1a\x00\\\"'", uint(42), uint8(8), uint16(16), uint32(32), uint64(64), "useless"},
"SELECT * FROM `a?` WHERE name = \"?\" AND state IN ('\\r\\n\\b\\t\\Z\\0\\\\\\\"''', '?', 42, 8, 16, 32, 64)", nil,
},
{
CQL,
"SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?", []interface{}{true, false, float32(1.234567), float64(9.87654321), []byte(nil), []byte("I'm bytes"), dt, time.Time{}, nil},
"SELECT TRUE, FALSE, 1.234567, 9.87654321, NULL, 0x49276D206279746573, '2019-04-24 12:23:34.123457+0800', '0000-00-00', NULL", nil,
},
{
CQL,
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\?", []interface{}{CQL},
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\'CQL'", nil,
},
{
CQL,
"SELECT ?", nil,
"", ErrInterpolateMissingArgs,
},
{
CQL,
"SELECT ?", []interface{}{complex(1, 2)},
"", ErrInterpolateUnsupportedArgs,
},
}
for idx, c := range cases {
a.Use(&idx, &c)
query, err := c.flavor.Interpolate(c.sql, c.args)
t.Run(fmt.Sprintf("%s: %s", c.flavor.String(), c.query), func(t *testing.T) {
a := assert.New(t)
a.Use(&idx, &c)
query, err := c.flavor.Interpolate(c.sql, c.args)
a.Equal(query, c.query)
a.Assert(err == c.err || err.Error() == c.err.Error())
a.Equal(query, c.query)
a.Assert(err == c.err || err.Error() == c.err.Error())
})
}
}
+5
View File
@@ -72,6 +72,11 @@ func Raw(expr string) interface{} {
return rawArgs{expr}
}
// Now returns a raw value comprising the NOW() function.
func Now() interface{} {
return Raw("NOW()")
}
type listArgs struct {
args []interface{}
}
+5
View File
@@ -310,6 +310,11 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
buf.WriteString(strconv.Itoa(sb.offset))
}
}
case CQL:
if sb.limit >= 0 {
buf.WriteString(" LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
}
case PostgreSQL:
if sb.limit >= 0 {
buf.WriteString(" LIMIT ")
+10 -2
View File
@@ -112,7 +112,7 @@ func ExampleSelectBuilder_join() {
}
func ExampleSelectBuilder_limit_offset() {
flavors := []Flavor{MySQL, PostgreSQL, SQLite, SQLServer}
flavors := []Flavor{MySQL, PostgreSQL, SQLite, SQLServer, CQL}
results := make([][]string, len(flavors))
sb := NewSelectBuilder()
saveResults := func() {
@@ -137,13 +137,15 @@ func ExampleSelectBuilder_limit_offset() {
// MySQL and SQLite: Ignore offset if the limit is not set.
// PostgreSQL: Offset can be set without limit.
// SQLServer: Offset can be set without limit.
// CQL: Ignore offset.
sb.Limit(-1)
sb.Offset(0)
saveResults()
// Case #3: limit >= 0 and offset >= 0
//
// All: Set both limit and offset.
// CQL: Ignore offset.
// All others: Set both limit and offset.
sb.Limit(1)
sb.Offset(0)
saveResults()
@@ -189,6 +191,12 @@ func ExampleSelectBuilder_limit_offset() {
// #2: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS
// #3: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
// #4: SELECT * FROM user ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY
//
// CQL
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1
// #4: SELECT * FROM user LIMIT 1
}
func ExampleSelectBuilder_ForUpdate() {
+10 -6
View File
@@ -114,12 +114,16 @@ func (s *Struct) SelectFromForTag(table string, tag string) *SelectBuilder {
buf := &bytes.Buffer{}
cols := make([]string, 0, len(fields))
for _, field := range fields {
buf.WriteString(table)
buf.WriteRune('.')
buf.WriteString(field)
cols = append(cols, buf.String())
buf.Reset()
if s.Flavor == CQL {
cols = append(cols, fields...)
} else {
for _, field := range fields {
buf.WriteString(table)
buf.WriteRune('.')
buf.WriteString(field)
cols = append(cols, buf.String())
buf.Reset()
}
}
sb.Select(cols...)
+27
View File
@@ -516,6 +516,21 @@ func ExampleStruct_forPostgreSQL() {
// [1234]
}
func ExampleStruct_forCQL() {
userStruct := NewStruct(new(User)).For(CQL)
sb := userStruct.SelectFrom("user")
sb.Where(sb.E("id", 1234))
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id, name, status FROM user WHERE id = ?
// [1234]
}
type structWithQuote struct {
A string `db:"aa" fieldopt:"withquote"`
B int `db:"-" fieldopt:"withquote"` // fieldopt is ignored as db is "-".
@@ -532,6 +547,10 @@ func TestStructWithQuote(t *testing.T) {
sql, _ = sb.Build()
a.Equal(sql, `SELECT foo."aa", foo.ccc FROM foo`)
sb = NewStruct(new(structWithQuote)).For(CQL).SelectFrom("foo")
sql, _ = sb.Build()
a.Equal(sql, "SELECT 'aa', ccc FROM foo")
ub := NewStruct(new(structWithQuote)).For(MySQL).Update("foo", structWithQuote{A: "aaa"})
sql, _ = ub.Build()
a.Equal(sql, "UPDATE foo SET `aa` = ?, ccc = ?")
@@ -540,6 +559,10 @@ func TestStructWithQuote(t *testing.T) {
sql, _ = ub.Build()
a.Equal(sql, `UPDATE foo SET "aa" = $1, ccc = $2`)
ub = NewStruct(new(structWithQuote)).For(CQL).Update("foo", structWithQuote{A: "aaa"})
sql, _ = ub.Build()
a.Equal(sql, `UPDATE foo SET 'aa' = ?, ccc = ?`)
ib := NewStruct(new(structWithQuote)).For(MySQL).InsertInto("foo", structWithQuote{A: "aaa"})
sql, _ = ib.Build()
a.Equal(sql, "INSERT INTO foo (`aa`, ccc) VALUES (?, ?)")
@@ -547,6 +570,10 @@ func TestStructWithQuote(t *testing.T) {
ib = NewStruct(new(structWithQuote)).For(PostgreSQL).InsertInto("foo", structWithQuote{A: "aaa"})
sql, _ = ib.Build()
a.Equal(sql, `INSERT INTO foo ("aa", ccc) VALUES ($1, $2)`)
ib = NewStruct(new(structWithQuote)).For(CQL).InsertInto("foo", structWithQuote{A: "aaa"})
sql, _ = ib.Build()
a.Equal(sql, "INSERT INTO foo ('aa', ccc) VALUES (?, ?)")
}
type structOmitEmpty struct {
+15
View File
@@ -17,6 +17,7 @@ const (
updateMarkerAfterWhere
updateMarkerAfterOrderBy
updateMarkerAfterLimit
updateMarkerAfterIf
)
// NewUpdateBuilder creates a new UPDATE builder.
@@ -46,6 +47,7 @@ type UpdateBuilder struct {
orderByCols []string
order string
limit int
ifExprs []string
args *Args
@@ -157,6 +159,13 @@ func (ub *UpdateBuilder) Limit(limit int) *UpdateBuilder {
return ub
}
// If sets IF expressions for UPDATE.
func (ub *UpdateBuilder) If(andExpr ...string) *UpdateBuilder {
ub.ifExprs = append(ub.ifExprs, andExpr...)
ub.marker = updateMarkerAfterIf
return ub
}
// String returns the compiled UPDATE string.
func (ub *UpdateBuilder) String() string {
s, _ := ub.Build()
@@ -207,6 +216,12 @@ func (ub *UpdateBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
ub.injection.WriteTo(buf, updateMarkerAfterLimit)
}
if len(ub.ifExprs) > 0 {
buf.WriteString(" IF ")
buf.WriteString(strings.Join(ub.ifExprs, " AND "))
ub.injection.WriteTo(buf, updateMarkerAfterIf)
}
return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
+6 -2
View File
@@ -44,14 +44,18 @@ func ExampleUpdateBuilder() {
"modified_at > created_at + "+ub.Var(86400), // It's allowed to write arbitrary SQL.
)
ub.OrderBy("id").Asc()
ub.If(
ub.E("type", "sys"),
ub.E("ready", true),
)
sql, args := ub.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// UPDATE demo.user SET type = ?, credit = credit + 1, modified_at = UNIX_TIMESTAMP(NOW()) WHERE id > ? AND name LIKE ? AND (id_card IS NULL OR status IN (?, ?, ?)) AND modified_at > created_at + ? ORDER BY id ASC
// [sys 1234 %Du 1 2 5 86400]
// UPDATE demo.user SET type = ?, credit = credit + 1, modified_at = UNIX_TIMESTAMP(NOW()) WHERE id > ? AND name LIKE ? AND (id_card IS NULL OR status IN (?, ?, ?)) AND modified_at > created_at + ? ORDER BY id ASC IF type = ? AND ready = ?
// [sys 1234 %Du 1 2 5 86400 sys true]
}
func TestUpdateAssignments(t *testing.T) {