Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CQL support #85

Merged
merged 6 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 7 additions & 0 deletions flavor.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
PostgreSQL
SQLite
SQLServer
CQL
)

var (
Expand Down Expand Up @@ -49,6 +50,8 @@ func (f Flavor) String() string {
return "SQLite"
case SQLServer:
return "SQLServer"
case CQL:
return "CQL"
}

return "<invalid>"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions flavor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>
}
24 changes: 12 additions & 12 deletions insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

Expand Down
16 changes: 14 additions & 2 deletions interpolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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, '\'')
Expand Down Expand Up @@ -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, "\\\""...)
Expand Down
44 changes: 39 additions & 5 deletions interpolate_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package sqlbuilder

import (
"fmt"
"strconv"
"testing"
"time"
Expand All @@ -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 {
Expand Down Expand Up @@ -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 changes: 5 additions & 0 deletions modifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func Raw(expr string) interface{} {
return rawArgs{expr}
}

// Now returns a raw value comprising the NOW() function.
func Now() interface{} {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not recommended to add this method. There are tons of functions in every DBMS. NOW() is not the one which is extremely outstanding and must be added in this package.

BTW, SQLServer doesn't have NOW(). The equivalent one is SYSDATETIME().

return Raw("NOW()")
}

type listArgs struct {
args []interface{}
}
Expand Down
5 changes: 5 additions & 0 deletions select.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ")
Expand Down
12 changes: 10 additions & 2 deletions select_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down
16 changes: 10 additions & 6 deletions struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down
Loading