Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	README.md
#	args.go
#	flavor.go
#	insert_test.go
#	interpolate.go
#	interpolate_test.go
#	select_test.go
#	struct.go
#	struct_test.go
This commit is contained in:
Spencer Connaughton
2023-01-05 10:19:05 -05:00
10 changed files with 283 additions and 92 deletions
+2 -2
View File
@@ -9,7 +9,7 @@
- [Usage](#usage)
- [Basic usage](#basic-usage)
- [Pre-defined SQL builders](#pre-defined-sql-builders)
- [Build SQL for MySQL, PostgreSQL, SQLServer or SQLite](#build-sql-for-mysql-postgresql-sqlserver-or-sqlite)
- [Build SQL for MySQL, PostgreSQL, SQLServer, SQLite, CQL, or ClickHouse](#build-sql-for-mysql-postgresql-sqlserve-sqlite-or-clickhouse)
- [Using `Struct` as a light weight ORM](#using-struct-as-a-light-weight-orm)
- [Nested SQL](#nested-sql)
- [Use `sql.Named` in a builder](#use-sqlnamed-in-a-builder)
@@ -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, SQLite, or CQL
### Build SQL for MySQL, PostgreSQL, SQLServer, SQLite, CQL, or ClickHouse
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.
+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, CQL:
case MySQL, SQLite, CQL, ClickHouse:
buf.WriteRune('?')
case PostgreSQL:
fmt.Fprintf(buf, "$%d", len(values)+1)
+11 -3
View File
@@ -17,6 +17,7 @@ const (
SQLite
SQLServer
CQL
ClickHouse
)
var (
@@ -52,6 +53,8 @@ func (f Flavor) String() string {
return "SQLServer"
case CQL:
return "CQL"
case ClickHouse:
return "ClickHouse"
}
return "<invalid>"
@@ -74,6 +77,8 @@ func (f Flavor) Interpolate(sql string, args []interface{}) (string, error) {
return sqlserverInterpolate(sql, args...)
case CQL:
return cqlInterpolate(sql, args...)
case ClickHouse:
return clickhouseInterpolate(sql, args...)
}
return "", ErrInterpolateNotImplemented
@@ -124,11 +129,11 @@ func (f Flavor) NewUnionBuilder() *UnionBuilder {
// Quote adds quote for name to make sure the name can be used safely
// as table name or field name.
//
// * For MySQL, use back quote (`) to quote name;
// * For PostgreSQL, SQL Server and SQLite, use double quote (") to quote name.
// - For MySQL, use back quote (`) to quote name;
// - For PostgreSQL, SQL Server and SQLite, use double quote (") to quote name.
func (f Flavor) Quote(name string) string {
switch f {
case MySQL:
case MySQL, ClickHouse:
return fmt.Sprintf("`%s`", name)
case PostgreSQL, SQLServer, SQLite:
return fmt.Sprintf(`"%s"`, name)
@@ -153,6 +158,9 @@ func (f Flavor) PrepareInsertIgnore(table string, ib *InsertBuilder) {
case SQLite:
// see https://www.sqlite.org/lang_insert.html
ib.verb = "INSERT OR IGNORE"
case ClickHouse:
// see https://clickhouse.tech/docs/en/sql-reference/statements/insert-into/
ib.verb = "INSERT"
default:
// panic if the db flavor is not supported
panic(fmt.Errorf("unsupported db flavor: %s", ib.args.Flavor.String()))
+16
View File
@@ -113,6 +113,22 @@ func ExampleInsertBuilder_insertIgnore_sqlite() {
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
}
func ExampleInsertBuilder_insertIgnore_clickhouse() {
ib := ClickHouse.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)
sql, args := ib.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// INSERT INTO demo.user (id, name, status, created_at) VALUES (?, ?, ?, UNIX_TIMESTAMP(NOW())), (?, ?, ?, ?)
// [1 Huan Du 1 2 Charmy Liu 1 1234567890]
}
func ExampleInsertBuilder_replaceInto() {
ib := NewInsertBuilder()
ib.ReplaceInto("demo.user")
+115 -70
View File
@@ -4,7 +4,9 @@
package sqlbuilder
import (
"database/sql/driver"
"fmt"
"reflect"
"strconv"
"time"
"unicode"
@@ -389,83 +391,22 @@ func cqlInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(CQL, query, args...)
}
func clickhouseInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(ClickHouse, query, args...)
}
func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
switch v := arg.(type) {
case nil:
buf = append(buf, "NULL"...)
case bool:
if v {
buf = append(buf, "TRUE"...)
case driver.Valuer:
if val, err := v.Value(); err != nil {
return nil, err
} else {
buf = append(buf, "FALSE"...)
return encodeValue(buf, val, flavor)
}
case int:
buf = strconv.AppendInt(buf, int64(v), 10)
case int8:
buf = strconv.AppendInt(buf, int64(v), 10)
case int16:
buf = strconv.AppendInt(buf, int64(v), 10)
case int32:
buf = strconv.AppendInt(buf, int64(v), 10)
case int64:
buf = strconv.AppendInt(buf, v, 10)
case uint:
buf = strconv.AppendUint(buf, uint64(v), 10)
case uint8:
buf = strconv.AppendUint(buf, uint64(v), 10)
case uint16:
buf = strconv.AppendUint(buf, uint64(v), 10)
case uint32:
buf = strconv.AppendUint(buf, uint64(v), 10)
case uint64:
buf = strconv.AppendUint(buf, v, 10)
case float32:
buf = strconv.AppendFloat(buf, float64(v), 'g', -1, 32)
case float64:
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
case []byte:
if v == nil {
buf = append(buf, "NULL"...)
break
}
switch flavor {
case MySQL:
buf = append(buf, "_binary"...)
buf = quoteStringValue(buf, *(*string)(unsafe.Pointer(&v)), flavor)
case PostgreSQL:
buf = append(buf, "E'\\\\x"...)
buf = appendHex(buf, v)
buf = append(buf, "'::bytea"...)
case SQLite:
buf = append(buf, "X'"...)
buf = appendHex(buf, v)
buf = append(buf, '\'')
case SQLServer, CQL:
buf = append(buf, "0x"...)
buf = appendHex(buf, v)
}
case string:
buf = quoteStringValue(buf, v, flavor)
case time.Time:
if v.IsZero() {
buf = append(buf, "'0000-00-00'"...)
@@ -492,6 +433,9 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
case CQL:
buf = append(buf, v.Format("2006-01-02 15:04:05.999999Z0700")...)
case ClickHouse:
buf = append(buf, v.Format("2006-01-02 15:04:05.999999")...)
}
buf = append(buf, '\'')
@@ -500,7 +444,108 @@ func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
buf = quoteStringValue(buf, v.String(), flavor)
default:
return nil, ErrInterpolateUnsupportedArgs
primative := reflect.ValueOf(arg)
switch k := primative.Kind(); k {
case reflect.Bool:
if primative.Bool() {
buf = append(buf, "TRUE"...)
} else {
buf = append(buf, "FALSE"...)
}
case reflect.Int:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int8:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int16:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int32:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int64:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Uint:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint8:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint16:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint32:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint64:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Float32:
buf = strconv.AppendFloat(buf, primative.Float(), 'g', -1, 32)
case reflect.Float64:
buf = strconv.AppendFloat(buf, primative.Float(), 'g', -1, 64)
case reflect.String:
buf = quoteStringValue(buf, primative.String(), flavor)
case reflect.Slice, reflect.Array:
if k == reflect.Slice && primative.IsNil() {
buf = append(buf, "NULL"...)
break
}
if elem := primative.Type().Elem(); elem.Kind() != reflect.Uint8 {
return nil, ErrInterpolateUnsupportedArgs
}
var data []byte
// Bytes() will panic if primative is an array and cannot be addressed.
// Copy all bytes to data as a fallback.
if k == reflect.Array && !primative.CanAddr() {
l := primative.Len()
data = make([]byte, l)
for i := 0; i < l; i++ {
data[i] = byte(primative.Index(i).Uint())
}
} else {
data = primative.Bytes()
}
switch flavor {
case MySQL:
buf = append(buf, "_binary"...)
buf = quoteStringValue(buf, *(*string)(unsafe.Pointer(&data)), flavor)
case PostgreSQL:
buf = append(buf, "E'\\\\x"...)
buf = appendHex(buf, data)
buf = append(buf, "'::bytea"...)
case SQLite:
buf = append(buf, "X'"...)
buf = appendHex(buf, data)
buf = append(buf, '\'')
case SQLServer, CQL:
buf = append(buf, "0x"...)
buf = appendHex(buf, data)
case ClickHouse:
buf = append(buf, "unhex('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
}
default:
return nil, ErrInterpolateUnsupportedArgs
}
}
return buf, nil
+80 -10
View File
@@ -1,6 +1,8 @@
package sqlbuilder
import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
"testing"
@@ -9,15 +11,24 @@ import (
"github.com/huandu/go-assert"
)
type errorValuer int
var ErrErrorValuer = errors.New("error valuer")
func (v errorValuer) Value() (driver.Value, error) {
return 0, ErrErrorValuer
}
func TestFlavorInterpolate(t *testing.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)
byteArr := [...]byte{'f', 'o', 'o'}
cases := []struct {
flavor Flavor
sql string
args []interface{}
query string
err error
Flavor Flavor
SQL string
Args []interface{}
Query string
Err error
}{
{
MySQL,
@@ -39,6 +50,11 @@ func TestFlavorInterpolate(t *testing.T) {
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\?", []interface{}{MySQL},
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\'MySQL'", nil,
},
{
MySQL,
"SELECT ?", []interface{}{byteArr},
"SELECT _binary'foo'", nil,
},
{
MySQL,
"SELECT ?", nil,
@@ -49,6 +65,16 @@ func TestFlavorInterpolate(t *testing.T) {
"SELECT ?", []interface{}{complex(1, 2)},
"", ErrInterpolateUnsupportedArgs,
},
{
MySQL,
"SELECT ?", []interface{}{[]complex128{complex(1, 2)}},
"", ErrInterpolateUnsupportedArgs,
},
{
MySQL,
"SELECT ?", []interface{}{errorValuer(1)},
"", ErrErrorValuer,
},
{
PostgreSQL,
@@ -137,7 +163,6 @@ 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)},
@@ -168,16 +193,61 @@ func TestFlavorInterpolate(t *testing.T) {
"SELECT ?", []interface{}{complex(1, 2)},
"", ErrInterpolateUnsupportedArgs,
},
{
ClickHouse,
"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,
},
{
ClickHouse,
"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,
},
{
ClickHouse,
"SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?", []interface{}{true, false, float32(1.234567), 9.87654321, []byte(nil), []byte("I'm bytes"), dt, time.Time{}, nil},
"SELECT TRUE, FALSE, 1.234567, 9.87654321, NULL, unhex('49276D206279746573'), '2019-04-24 12:23:34.123457', '0000-00-00', NULL", nil,
},
{
ClickHouse,
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\?", []interface{}{MySQL},
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\'MySQL'", nil,
},
{
ClickHouse,
"SELECT ?", []interface{}{byteArr},
"SELECT unhex('666F6F')", nil,
},
{
ClickHouse,
"SELECT ?", nil,
"", ErrInterpolateMissingArgs,
},
{
ClickHouse,
"SELECT ?", []interface{}{complex(1, 2)},
"", ErrInterpolateUnsupportedArgs,
},
{
ClickHouse,
"SELECT ?", []interface{}{[]complex128{complex(1, 2)}},
"", ErrInterpolateUnsupportedArgs,
},
{
ClickHouse,
"SELECT ?", []interface{}{errorValuer(1)},
"", ErrErrorValuer,
},
}
for idx, c := range cases {
t.Run(fmt.Sprintf("%s: %s", c.flavor.String(), c.query), func(t *testing.T) {
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)
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())
})
}
}
+1 -1
View File
@@ -300,7 +300,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
}
switch flavor {
case MySQL, SQLite:
case MySQL, SQLite, ClickHouse:
if sb.limit >= 0 {
buf.WriteString(" LIMIT ")
buf.WriteString(strconv.Itoa(sb.limit))
+7 -1
View File
@@ -112,7 +112,7 @@ func ExampleSelectBuilder_join() {
}
func ExampleSelectBuilder_limit_offset() {
flavors := []Flavor{MySQL, PostgreSQL, SQLite, SQLServer, CQL}
flavors := []Flavor{MySQL, PostgreSQL, SQLite, SQLServer, CQL, ClickHouse}
results := make([][]string, len(flavors))
sb := NewSelectBuilder()
saveResults := func() {
@@ -197,6 +197,12 @@ func ExampleSelectBuilder_limit_offset() {
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1
// #4: SELECT * FROM user LIMIT 1
//
// ClickHouse
// #1: SELECT * FROM user
// #2: SELECT * FROM user
// #3: SELECT * FROM user LIMIT 1 OFFSET 0
// #4: SELECT * FROM user LIMIT 1
}
func ExampleSelectBuilder_ForUpdate() {
+18 -4
View File
@@ -5,6 +5,7 @@ package sqlbuilder
import (
"bytes"
"database/sql/driver"
"math"
"reflect"
"regexp"
@@ -36,6 +37,8 @@ const (
var optRegex = regexp.MustCompile(`(?P<` + optName + `>\w+)(\((?P<` + optParams + `>.*)\))?`)
var typeOfSQLDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
// Struct represents a struct type.
//
// All methods in Struct are thread-safe.
@@ -135,7 +138,6 @@ func (s *Struct) SelectFromForTag(table string, tag string) (sb *SelectBuilder)
}
sb.Select(cols...)
return sb
}
@@ -182,7 +184,7 @@ func (s *Struct) UpdateForTag(table string, tag string, value interface{}) *Upda
continue
}
} else {
val = dereferencedValue(val)
val = dereferencedFieldValue(val)
}
data := val.Interface()
@@ -240,7 +242,7 @@ func (s *Struct) buildColsAndValuesForTag(ib *InsertBuilder, tag string, value .
for _, item := range value {
v := reflect.ValueOf(item)
v = dereferencedValue(v)
v = dereferencedFieldValue(v)
if v.Type() == s.structType {
vs = append(vs, v)
@@ -268,7 +270,7 @@ func (s *Struct) buildColsAndValuesForTag(ib *InsertBuilder, tag string, value .
nilCnt++
}
val = dereferencedValue(val)
val = dereferencedFieldValue(val)
if val.IsValid() {
values[i] = append(values[i], val.Interface())
@@ -488,6 +490,18 @@ func dereferencedValue(v reflect.Value) reflect.Value {
return v
}
func dereferencedFieldValue(v reflect.Value) reflect.Value {
for k := v.Kind(); k == reflect.Ptr || k == reflect.Interface; k = v.Kind() {
if v.Type().Implements(typeOfSQLDriverValuer) {
break
}
v = v.Elem()
}
return v
}
// isEmptyValue checks if v is zero.
// Following code is borrowed from `IsZero` method in `reflect.Value` since Go 1.13.
func isEmptyValue(v reflect.Value) bool {
+32
View File
@@ -4,6 +4,7 @@
package sqlbuilder
import (
"database/sql/driver"
"fmt"
"testing"
"time"
@@ -838,6 +839,37 @@ func TestStructFieldAs(t *testing.T) {
a.Equal(sql, `UPDATE t SET t1 = ?, t2 = ?, t4 = ?`)
}
type structImplValuer int
func (v *structImplValuer) Value() (driver.Value, error) {
return *v * 2, nil
}
type structContainsValuer struct {
F1 string
F2 *structImplValuer
}
func TestStructFieldsImplValuer(t *testing.T) {
a := assert.New(t)
st := NewStruct(new(structContainsValuer))
f1 := "foo"
f2 := structImplValuer(100)
sql, args := st.Update("t", structContainsValuer{
F1: f1,
F2: &f2,
}).BuildWithFlavor(MySQL)
a.Equal(sql, "UPDATE t SET F1 = ?, F2 = ?")
a.Equal(args[0], f1)
a.Equal(args[1], &f2)
result, err := MySQL.Interpolate(sql, args)
a.NilError(err)
a.Equal(result, "UPDATE t SET F1 = 'foo', F2 = 200")
}
func SomeOtherMapper(string) string {
return ""
}