Skip to content

Commit 989b698

Browse files
authored
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.
1 parent 6ef7d0b commit 989b698

16 files changed

+206
-32
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Following are some utility methods to deal with special cases.
110110

111111
To learn how to use builders, check out [examples on GoDoc](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#pkg-examples).
112112

113-
### Build SQL for MySQL, PostgreSQL, SQLServer or SQLite
113+
### Build SQL for MySQL, PostgreSQL, SQLServer, SQLite, or CQL
114114

115115
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.
116116

@@ -122,7 +122,7 @@ We can wrap any `Builder` with a default flavor through `WithFlavor`.
122122

123123
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.
124124

125-
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.
125+
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.
126126

127127
### Using `Struct` as a light weight ORM
128128

args.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func (args *Args) compileArg(buf *bytes.Buffer, flavor Flavor, values []interfac
233233
}
234234
default:
235235
switch flavor {
236-
case MySQL, SQLite:
236+
case MySQL, SQLite, CQL:
237237
buf.WriteRune('?')
238238
case PostgreSQL:
239239
fmt.Fprintf(buf, "$%d", len(values)+1)

args_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ func TestArgs(t *testing.T) {
7777

7878
a.Equal(actual, expected)
7979
}
80+
81+
DefaultFlavor = CQL
82+
83+
for expected, c := range cases {
84+
args := new(Args)
85+
86+
for i := 1; i < len(c); i++ {
87+
args.Add(c[i])
88+
}
89+
90+
sql, values := args.Compile(c[0].(string))
91+
actual := fmt.Sprintf("%v\n%v", sql, values)
92+
93+
a.Equal(actual, expected)
94+
}
8095
}
8196

8297
func toPostgreSQL(sql string) string {

builder_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,24 @@ func TestBuildWithPostgreSQL(t *testing.T) {
103103
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")
104104
a.Equal(args, []interface{}{7890, 1234, 2, 4567, 5})
105105
}
106+
107+
func TestBuildWithCQL(t *testing.T) {
108+
a := assert.New(t)
109+
110+
ib1 := CQL.NewInsertBuilder()
111+
ib1.InsertInto("t1").Cols("col1", "col2").Values(1, 2)
112+
113+
ib2 := CQL.NewInsertBuilder()
114+
ib2.InsertInto("t2").Cols("col3", "col4").Values(3, 4)
115+
116+
old := DefaultFlavor
117+
DefaultFlavor = CQL
118+
defer func() {
119+
DefaultFlavor = old
120+
}()
121+
122+
sql, args := Build("BEGIN BATCH USING TIMESTAMP $0 $1; $2; APPLY BATCH;", 1481124356754405, ib1, ib2).Build()
123+
124+
a.Equal(sql, "BEGIN BATCH USING TIMESTAMP ? INSERT INTO t1 (col1, col2) VALUES (?, ?); INSERT INTO t2 (col3, col4) VALUES (?, ?); APPLY BATCH;")
125+
a.Equal(args, []interface{}{1481124356754405, 1, 2, 3, 4})
126+
}

flavor.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
PostgreSQL
1717
SQLite
1818
SQLServer
19+
CQL
1920
)
2021

2122
var (
@@ -49,6 +50,8 @@ func (f Flavor) String() string {
4950
return "SQLite"
5051
case SQLServer:
5152
return "SQLServer"
53+
case CQL:
54+
return "CQL"
5255
}
5356

5457
return "<invalid>"
@@ -69,6 +72,8 @@ func (f Flavor) Interpolate(sql string, args []interface{}) (string, error) {
6972
return sqliteInterpolate(sql, args...)
7073
case SQLServer:
7174
return sqlserverInterpolate(sql, args...)
75+
case CQL:
76+
return cqlInterpolate(sql, args...)
7277
}
7378

7479
return "", ErrInterpolateNotImplemented
@@ -127,6 +132,8 @@ func (f Flavor) Quote(name string) string {
127132
return fmt.Sprintf("`%s`", name)
128133
case PostgreSQL, SQLServer, SQLite:
129134
return fmt.Sprintf(`"%s"`, name)
135+
case CQL:
136+
return fmt.Sprintf("'%s'", name)
130137
}
131138

132139
return name

flavor_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,20 @@ func ExampleFlavor_Interpolate_sqlServer() {
121121
// SELECT name FROM user WHERE id <> 1234 AND name = N'Charmy Liu' AND desc LIKE N'%mother\'s day%'
122122
// <nil>
123123
}
124+
125+
func ExampleFlavor_Interpolate_cql() {
126+
sb := CQL.NewSelectBuilder()
127+
sb.Select("name").From("user").Where(
128+
sb.E("id", 1234),
129+
sb.E("name", "Charmy Liu"),
130+
)
131+
sql, args := sb.Build()
132+
query, err := CQL.Interpolate(sql, args)
133+
134+
fmt.Println(query)
135+
fmt.Println(err)
136+
137+
// Output:
138+
// SELECT name FROM user WHERE id = 1234 AND name = 'Charmy Liu'
139+
// <nil>
140+
}

insert_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,48 +52,48 @@ func ExampleReplaceInto() {
5252
func ExampleInsertBuilder() {
5353
ib := NewInsertBuilder()
5454
ib.InsertInto("demo.user")
55-
ib.Cols("id", "name", "status", "created_at")
56-
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
57-
ib.Values(2, "Charmy Liu", 1, 1234567890)
55+
ib.Cols("id", "name", "status", "created_at", "updated_at")
56+
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
57+
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
5858

5959
sql, args := ib.Build()
6060
fmt.Println(sql)
6161
fmt.Println(args)
6262

6363
// Output:
64-
// INSERT INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
64+
// INSERT INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
6565
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
6666
}
6767

6868
func ExampleInsertBuilder_insertIgnore() {
6969
ib := NewInsertBuilder()
7070
ib.InsertIgnoreInto("demo.user")
71-
ib.Cols("id", "name", "status", "created_at")
72-
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
73-
ib.Values(2, "Charmy Liu", 1, 1234567890)
71+
ib.Cols("id", "name", "status", "created_at", "updated_at")
72+
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
73+
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
7474

7575
sql, args := ib.Build()
7676
fmt.Println(sql)
7777
fmt.Println(args)
7878

7979
// Output:
80-
// INSERT IGNORE INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
80+
// INSERT IGNORE INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
8181
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
8282
}
8383

8484
func ExampleInsertBuilder_replaceInto() {
8585
ib := NewInsertBuilder()
8686
ib.ReplaceInto("demo.user")
87-
ib.Cols("id", "name", "status", "created_at")
88-
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"))
89-
ib.Values(2, "Charmy Liu", 1, 1234567890)
87+
ib.Cols("id", "name", "status", "created_at", "updated_at")
88+
ib.Values(1, "Huan Du", 1, Raw("UNIX_TIMESTAMP(NOW())"), Now())
89+
ib.Values(2, "Charmy Liu", 1, 1234567890, Now())
9090

9191
sql, args := ib.Build()
9292
fmt.Println(sql)
9393
fmt.Println(args)
9494

9595
// Output:
96-
// REPLACE INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
96+
// REPLACE INTO demo.user (id, name, status, created_at, updated_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW()), NOW()), (?, ?, ?, ?, NOW())
9797
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
9898
}
9999

interpolate.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,11 @@ func sqliteInterpolate(query string, args ...interface{}) (string, error) {
384384
return mysqlLikeInterpolate(SQLite, query, args...)
385385
}
386386

387+
// cqlInterpolate works the same as MySQL interpolating.
388+
func cqlInterpolate(query string, args ...interface{}) (string, error) {
389+
return mysqlLikeInterpolate(CQL, query, args...)
390+
}
391+
387392
func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
388393
switch v := arg.(type) {
389394
case nil:
@@ -453,7 +458,7 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
453458
buf = appendHex(buf, v)
454459
buf = append(buf, '\'')
455460

456-
case SQLServer:
461+
case SQLServer, CQL:
457462
buf = append(buf, "0x"...)
458463
buf = appendHex(buf, v)
459464
}
@@ -484,6 +489,9 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
484489

485490
case SQLServer:
486491
buf = append(buf, v.Format("2006-01-02 15:04:05.999999 Z07:00")...)
492+
493+
case CQL:
494+
buf = append(buf, v.Format("2006-01-02 15:04:05.999999Z0700")...)
487495
}
488496

489497
buf = append(buf, '\'')
@@ -540,7 +548,11 @@ func quoteStringValue(buf []byte, s string, flavor Flavor) []byte {
540548
buf = append(buf, "\\Z"...)
541549

542550
case '\'':
543-
buf = append(buf, "\\'"...)
551+
if flavor == CQL {
552+
buf = append(buf, "''"...)
553+
} else {
554+
buf = append(buf, "\\'"...)
555+
}
544556

545557
case '"':
546558
buf = append(buf, "\\\""...)

interpolate_test.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sqlbuilder
22

33
import (
4+
"fmt"
45
"strconv"
56
"testing"
67
"time"
@@ -9,7 +10,6 @@ import (
910
)
1011

1112
func TestFlavorInterpolate(t *testing.T) {
12-
a := assert.New(t)
1313
dt := time.Date(2019, 4, 24, 12, 23, 34, 123456789, time.FixedZone("CST", 8*60*60)) // 2019-04-24 12:23:34.987654321 CST
1414
_, errOutOfRange := strconv.ParseInt("12345678901234567890", 10, 32)
1515
cases := []struct {
@@ -137,13 +137,47 @@ func TestFlavorInterpolate(t *testing.T) {
137137
"SELECT @p1", nil,
138138
"", ErrInterpolateMissingArgs,
139139
},
140+
141+
{
142+
CQL,
143+
"SELECT * FROM a WHERE name = ? AND state IN (?, ?, ?, ?, ?)", []interface{}{"I'm fine", 42, int8(8), int16(-16), int32(32), int64(64)},
144+
"SELECT * FROM a WHERE name = 'I''m fine' AND state IN (42, 8, -16, 32, 64)", nil,
145+
},
146+
{
147+
CQL,
148+
"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"},
149+
"SELECT * FROM `a?` WHERE name = \"?\" AND state IN ('\\r\\n\\b\\t\\Z\\0\\\\\\\"''', '?', 42, 8, 16, 32, 64)", nil,
150+
},
151+
{
152+
CQL,
153+
"SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?", []interface{}{true, false, float32(1.234567), float64(9.87654321), []byte(nil), []byte("I'm bytes"), dt, time.Time{}, nil},
154+
"SELECT TRUE, FALSE, 1.234567, 9.87654321, NULL, 0x49276D206279746573, '2019-04-24 12:23:34.123457+0800', '0000-00-00', NULL", nil,
155+
},
156+
{
157+
CQL,
158+
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\?", []interface{}{CQL},
159+
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\'CQL'", nil,
160+
},
161+
{
162+
CQL,
163+
"SELECT ?", nil,
164+
"", ErrInterpolateMissingArgs,
165+
},
166+
{
167+
CQL,
168+
"SELECT ?", []interface{}{complex(1, 2)},
169+
"", ErrInterpolateUnsupportedArgs,
170+
},
140171
}
141172

142173
for idx, c := range cases {
143-
a.Use(&idx, &c)
144-
query, err := c.flavor.Interpolate(c.sql, c.args)
174+
t.Run(fmt.Sprintf("%s: %s", c.flavor.String(), c.query), func(t *testing.T) {
175+
a := assert.New(t)
176+
a.Use(&idx, &c)
177+
query, err := c.flavor.Interpolate(c.sql, c.args)
145178

146-
a.Equal(query, c.query)
147-
a.Assert(err == c.err || err.Error() == c.err.Error())
179+
a.Equal(query, c.query)
180+
a.Assert(err == c.err || err.Error() == c.err.Error())
181+
})
148182
}
149183
}

modifiers.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ func Raw(expr string) interface{} {
7272
return rawArgs{expr}
7373
}
7474

75+
// Now returns a raw value comprising the NOW() function.
76+
func Now() interface{} {
77+
return Raw("NOW()")
78+
}
79+
7580
type listArgs struct {
7681
args []interface{}
7782
}

0 commit comments

Comments
 (0)