mirror of
https://github.com/hanzoai/sqlbuilder.git
synced 2026-10-01 00:37:42 +00:00
new feature: interpolate args in sql
This commit is contained in:
@@ -238,6 +238,32 @@ fmt.Println(args)
|
||||
|
||||
If we just want to use `${name}` syntax to refer named arguments, use `BuildNamed` instead. It disables all special syntax but `${name}` and `$$`.
|
||||
|
||||
### Interpolate `args` in the `sql` ###
|
||||
|
||||
Some SQL drivers doesn't actually implement `StmtExecContext#ExecContext`. They will fail when `len(args) > 0`. The only solution is to interpolate `args` in the `sql`, and execute the interpolated query with the driver.
|
||||
|
||||
*Security warning*: I try my best to escape special characters in interpolate methods, but it's still less secure than `Stmt` implemented by SQL drivers. If `Stmt` is well supported, it's highly recommended to use `Stmt` instead of this feature.
|
||||
|
||||
This feature is inspired by package `github.com/go-sql-driver/mysql`.
|
||||
|
||||
```go
|
||||
sb := MySQL.NewSelectBuilder()
|
||||
sb.Select("name").From("user").Where(
|
||||
sb.NE("id", 1234),
|
||||
sb.E("name", "Charmy Liu"),
|
||||
sb.Like("desc", "%mother's day%"),
|
||||
)
|
||||
sql, args := sb.Build()
|
||||
query, err := MySQL.Interpolate(sql, args)
|
||||
|
||||
fmt.Println(query)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output:
|
||||
// SELECT name FROM user WHERE id <> 1234 AND name = 'Charmy Liu' AND desc LIKE '%mother\'s day%'
|
||||
// <nil>
|
||||
```
|
||||
|
||||
## FAQ ##
|
||||
|
||||
### What's the difference between this package and `squirrel` ###
|
||||
|
||||
@@ -23,9 +23,28 @@ type Args struct {
|
||||
onlyNamed bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Predefine some $n args to avoid additional memory allocation.
|
||||
predefinedArgs = make([]string, 0, maxPredefinedArgs)
|
||||
|
||||
for i := 0; i < maxPredefinedArgs; i++ {
|
||||
predefinedArgs = append(predefinedArgs, fmt.Sprintf("$%v", i))
|
||||
}
|
||||
}
|
||||
|
||||
const maxPredefinedArgs = 1024
|
||||
|
||||
var predefinedArgs []string
|
||||
|
||||
// Add adds an arg to Args and returns a placeholder.
|
||||
func (args *Args) Add(arg interface{}) string {
|
||||
return fmt.Sprintf("$%v", args.add(arg))
|
||||
idx := args.add(arg)
|
||||
|
||||
if idx < maxPredefinedArgs {
|
||||
return predefinedArgs[idx]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("$%v", idx)
|
||||
}
|
||||
|
||||
func (args *Args) add(arg interface{}) int {
|
||||
|
||||
@@ -15,7 +15,7 @@ type Cond struct {
|
||||
|
||||
// Equal represents "field = value".
|
||||
func (c *Cond) Equal(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v = %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s = %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// E is an alias of Equal.
|
||||
@@ -25,7 +25,7 @@ func (c *Cond) E(field string, value interface{}) string {
|
||||
|
||||
// NotEqual represents "field != value".
|
||||
func (c *Cond) NotEqual(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v <> %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s <> %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// NE is an alias of NotEqual.
|
||||
@@ -35,7 +35,7 @@ func (c *Cond) NE(field string, value interface{}) string {
|
||||
|
||||
// GreaterThan represents "field > value".
|
||||
func (c *Cond) GreaterThan(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v > %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s > %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// G is an alias of GreaterThan.
|
||||
@@ -45,7 +45,7 @@ func (c *Cond) G(field string, value interface{}) string {
|
||||
|
||||
// GreaterEqualThan represents "field >= value".
|
||||
func (c *Cond) GreaterEqualThan(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v >= %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s >= %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// GE is an alias of GreaterEqualThan.
|
||||
@@ -55,7 +55,7 @@ func (c *Cond) GE(field string, value interface{}) string {
|
||||
|
||||
// LessThan represents "field < value".
|
||||
func (c *Cond) LessThan(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v < %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s < %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// L is an alias of LessThan.
|
||||
@@ -65,7 +65,7 @@ func (c *Cond) L(field string, value interface{}) string {
|
||||
|
||||
// LessEqualThan represents "field <= value".
|
||||
func (c *Cond) LessEqualThan(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v <= %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s <= %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// LE is an alias of LessEqualThan.
|
||||
@@ -81,7 +81,7 @@ func (c *Cond) In(field string, value ...interface{}) string {
|
||||
vs = append(vs, c.Args.Add(v))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v IN (%v)", Escape(field), strings.Join(vs, ", "))
|
||||
return fmt.Sprintf("%s IN (%s)", Escape(field), strings.Join(vs, ", "))
|
||||
}
|
||||
|
||||
// NotIn represents "field NOT IN (value...)".
|
||||
@@ -92,47 +92,47 @@ func (c *Cond) NotIn(field string, value ...interface{}) string {
|
||||
vs = append(vs, c.Args.Add(v))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v NOT IN (%v)", Escape(field), strings.Join(vs, ", "))
|
||||
return fmt.Sprintf("%s NOT IN (%s)", Escape(field), strings.Join(vs, ", "))
|
||||
}
|
||||
|
||||
// Like represents "field LIKE value".
|
||||
func (c *Cond) Like(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v LIKE %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s LIKE %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// NotLike represents "field NOT LIKE value".
|
||||
func (c *Cond) NotLike(field string, value interface{}) string {
|
||||
return fmt.Sprintf("%v NOT LIKE %v", Escape(field), c.Args.Add(value))
|
||||
return fmt.Sprintf("%s NOT LIKE %s", Escape(field), c.Args.Add(value))
|
||||
}
|
||||
|
||||
// IsNull represents "field IS NULL".
|
||||
func (c *Cond) IsNull(field string) string {
|
||||
return fmt.Sprintf("%v IS NULL", Escape(field))
|
||||
return fmt.Sprintf("%s IS NULL", Escape(field))
|
||||
}
|
||||
|
||||
// IsNotNull represents "field IS NOT NULL".
|
||||
func (c *Cond) IsNotNull(field string) string {
|
||||
return fmt.Sprintf("%v IS NOT NULL", Escape(field))
|
||||
return fmt.Sprintf("%s IS NOT NULL", Escape(field))
|
||||
}
|
||||
|
||||
// Between represents "field BETWEEN lower AND upper".
|
||||
func (c *Cond) Between(field string, lower, upper interface{}) string {
|
||||
return fmt.Sprintf("%v BETWEEN %v AND %v", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
|
||||
return fmt.Sprintf("%s BETWEEN %s AND %s", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
|
||||
}
|
||||
|
||||
// NotBetween represents "field NOT BETWEEN lower AND upper".
|
||||
func (c *Cond) NotBetween(field string, lower, upper interface{}) string {
|
||||
return fmt.Sprintf("%v NOT BETWEEN %v AND %v", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
|
||||
return fmt.Sprintf("%s NOT BETWEEN %s AND %s", Escape(field), c.Args.Add(lower), c.Args.Add(upper))
|
||||
}
|
||||
|
||||
// Or represents OR logic like "expr1 OR expr2 OR expr3".
|
||||
func (c *Cond) Or(orExpr ...string) string {
|
||||
return fmt.Sprintf("(%v)", strings.Join(orExpr, " OR "))
|
||||
return fmt.Sprintf("(%s)", strings.Join(orExpr, " OR "))
|
||||
}
|
||||
|
||||
// And represents AND logic like "expr1 AND expr2 AND expr3".
|
||||
func (c *Cond) And(andExpr ...string) string {
|
||||
return fmt.Sprintf("(%v)", strings.Join(andExpr, " AND "))
|
||||
return fmt.Sprintf("(%s)", strings.Join(andExpr, " AND "))
|
||||
}
|
||||
|
||||
// Var returns a placeholder for value.
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package sqlbuilder
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Supported flavors.
|
||||
const (
|
||||
@@ -18,6 +21,18 @@ var (
|
||||
DefaultFlavor = MySQL
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInterpolateNotImplemented means the method or feature is not implemented right now.
|
||||
ErrInterpolateNotImplemented = errors.New("go-sqlbuilder: interpolation for this flavor is not implemented")
|
||||
|
||||
// ErrInterpolateMissingArgs means there are some args missing in query, so it's not possible to
|
||||
// prepare a query with such args.
|
||||
ErrInterpolateMissingArgs = errors.New("go-sqlbuilder: not enough args when interpolating")
|
||||
|
||||
// ErrInterpolateUnsupportedArgs means that some types of the args are not supported.
|
||||
ErrInterpolateUnsupportedArgs = errors.New("go-sqlbuilder: unsupported args when interpolating")
|
||||
)
|
||||
|
||||
// Flavor is the flag to control the format of compiled sql.
|
||||
type Flavor int
|
||||
|
||||
@@ -33,6 +48,22 @@ func (f Flavor) String() string {
|
||||
return "<invalid>"
|
||||
}
|
||||
|
||||
// Interpolate parses sql returned by `Args#Compile` or `Builder`,
|
||||
// and interpolate args to replace placeholders in the sql.
|
||||
//
|
||||
// If there are some args missing in sql, e.g. the number of placeholders are larger than len(args),
|
||||
// returns ErrMissingArgs error.
|
||||
func (f Flavor) Interpolate(sql string, args []interface{}) (string, error) {
|
||||
switch f {
|
||||
case MySQL:
|
||||
return mysqlInterpolate(sql, args...)
|
||||
case PostgreSQL:
|
||||
return postgresqlInterpolate(sql, args...)
|
||||
}
|
||||
|
||||
return "", ErrInterpolateNotImplemented
|
||||
}
|
||||
|
||||
// NewCreateTableBuilder creates a new CREATE TABLE builder with flavor.
|
||||
func (f Flavor) NewCreateTableBuilder() *CreateTableBuilder {
|
||||
b := newCreateTableBuilder()
|
||||
|
||||
+108
@@ -5,7 +5,9 @@ package sqlbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFlavor(t *testing.T) {
|
||||
@@ -22,6 +24,94 @@ func TestFlavor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
cases := []struct {
|
||||
flavor Flavor
|
||||
sql string
|
||||
args []interface{}
|
||||
query string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
MySQL,
|
||||
"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,
|
||||
},
|
||||
{
|
||||
MySQL,
|
||||
"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,
|
||||
},
|
||||
{
|
||||
MySQL,
|
||||
"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, _binary'I\\'m bytes', '2019-04-24 12:23:34.123457', '0000-00-00', NULL", nil,
|
||||
},
|
||||
{
|
||||
MySQL,
|
||||
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\?", []interface{}{MySQL},
|
||||
"SELECT '\\'?', \"\\\"?\", `\\`?`, \\'MySQL'", nil,
|
||||
},
|
||||
{
|
||||
MySQL,
|
||||
"SELECT ?", nil,
|
||||
"", ErrInterpolateMissingArgs,
|
||||
},
|
||||
{
|
||||
MySQL,
|
||||
"SELECT ?", []interface{}{complex(1, 2)},
|
||||
"", ErrInterpolateUnsupportedArgs,
|
||||
},
|
||||
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT * FROM a WHERE name = $3 AND state IN ($2, $4, $1, $6, $5)", []interface{}{"I'm fine", 42, int8(8), int16(-16), int32(32), int64(64)},
|
||||
"SELECT * FROM a WHERE name = 8 AND state IN (42, -16, 'I\\'m fine', 64, 32)", nil,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT * FROM $abc$$1$abc$1$1 WHERE name = \"$1\" AND state IN ($2, '$1', $3, $6, $5, $4, $2) $3", []interface{}{"\r\n\b\t\x1a\x00\\\"'", uint(42), uint8(8), uint16(16), uint32(32), uint64(64), "useless"},
|
||||
"SELECT * FROM $abc$$1$abc$1'\\r\\n\\b\\t\\Z\\0\\\\\\\"\\'' WHERE name = \"$1\" AND state IN (42, '$1', 8, 64, 32, 16, 42) 8", nil,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $11, $a", []interface{}{true, false, float32(1.234567), float64(9.87654321), []byte(nil), []byte("I'm bytes"), dt, time.Time{}, nil, 10, 11, 12},
|
||||
"SELECT TRUE, FALSE, 1.234567, 9.87654321, NULL, E'\\\\x49276D206279746573'::bytea, '2019-04-24 12:23:34.123457 CST', '0000-00-00', NULL, 11, $a", nil,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT '\\'$1', \"\\\"$1\", `$1`, \\$1a, $$1$$, $a $b$ $a $ $1$b$1$1 $a$ $", []interface{}{MySQL},
|
||||
"SELECT '\\'$1', \"\\\"$1\", `'MySQL'`, \\'MySQL'a, $$1$$, $a $b$ $a $ $1$b$1'MySQL' $a$ $", nil,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT $1", nil,
|
||||
"", ErrInterpolateMissingArgs,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT $1", []interface{}{complex(1, 2)},
|
||||
"", ErrInterpolateUnsupportedArgs,
|
||||
},
|
||||
{
|
||||
PostgreSQL,
|
||||
"SELECT $12345678901234567890", nil,
|
||||
"", errOutOfRange,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, c := range cases {
|
||||
query, err := c.flavor.Interpolate(c.sql, c.args)
|
||||
|
||||
if query != c.query || (err != c.err && err.Error() != c.err.Error()) {
|
||||
t.Fatalf("unexpected interpolate result. [idx:%v] [err:%v] [case:%#v]\n expected: %v\n actual: %v",
|
||||
idx, err, c, c.query, query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleFlavor() {
|
||||
// Create a flavored builder.
|
||||
sb := PostgreSQL.NewSelectBuilder()
|
||||
@@ -38,3 +128,21 @@ func ExampleFlavor() {
|
||||
// SELECT name FROM user WHERE id = $1 AND rank > $2
|
||||
// [1234 3]
|
||||
}
|
||||
|
||||
func ExampleFlavor_Interpolate() {
|
||||
sb := MySQL.NewSelectBuilder()
|
||||
sb.Select("name").From("user").Where(
|
||||
sb.NE("id", 1234),
|
||||
sb.E("name", "Charmy Liu"),
|
||||
sb.Like("desc", "%mother's day%"),
|
||||
)
|
||||
sql, args := sb.Build()
|
||||
query, err := MySQL.Interpolate(sql, args)
|
||||
|
||||
fmt.Println(query)
|
||||
fmt.Println(err)
|
||||
|
||||
// Output:
|
||||
// SELECT name FROM user WHERE id <> 1234 AND name = 'Charmy Liu' AND desc LIKE '%mother\'s day%'
|
||||
// <nil>
|
||||
}
|
||||
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
// Copyright 2018 Huan Du. All rights reserved.
|
||||
// Licensed under the MIT license that can be found in the LICENSE file.
|
||||
|
||||
package sqlbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// mysqlInterpolate parses query and replace all "?" with encoded args.
|
||||
// If there are more "?" than len(args), returns ErrMissingArgs.
|
||||
// Otherwise, if there are less "?" than len(args), the redundant args are omitted.
|
||||
func mysqlInterpolate(query string, args ...interface{}) (string, error) {
|
||||
// Roughly estimate the size to avoid useless memory allocation and copy.
|
||||
buf := make([]byte, 0, len(query)+len(args)*20)
|
||||
|
||||
var quote rune
|
||||
var err error
|
||||
cnt := 0
|
||||
max := len(args)
|
||||
escaping := false
|
||||
offset := 0
|
||||
target := query
|
||||
r, sz := utf8.DecodeRuneInString(target)
|
||||
|
||||
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
|
||||
offset += sz
|
||||
target = query[offset:]
|
||||
|
||||
if escaping {
|
||||
escaping = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '?':
|
||||
if quote != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if cnt >= max {
|
||||
return "", ErrInterpolateMissingArgs
|
||||
}
|
||||
|
||||
buf = append(buf, query[:offset-sz]...)
|
||||
buf, err = encodeValue(buf, args[cnt], MySQL)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
query = target
|
||||
offset = 0
|
||||
cnt++
|
||||
|
||||
case '\'':
|
||||
if quote == '\'' {
|
||||
quote = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if quote == 0 {
|
||||
quote = '\''
|
||||
}
|
||||
|
||||
case '"':
|
||||
if quote == '"' {
|
||||
quote = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if quote == 0 {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
case '`':
|
||||
if quote == '`' {
|
||||
quote = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if quote == 0 {
|
||||
quote = '`'
|
||||
}
|
||||
|
||||
case '\\':
|
||||
if quote != 0 {
|
||||
escaping = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf = append(buf, query...)
|
||||
return *(*string)(unsafe.Pointer(&buf)), nil
|
||||
}
|
||||
|
||||
// postgresqlInterpolate parses query and replace all "$*" with encoded args.
|
||||
// If there are more "$*" than len(args), returns ErrMissingArgs.
|
||||
// Otherwise, if there are less "$*" than len(args), the redundant args are omitted.
|
||||
func postgresqlInterpolate(query string, args ...interface{}) (string, error) {
|
||||
// Roughly estimate the size to avoid useless memory allocation and copy.
|
||||
buf := make([]byte, 0, len(query)+len(args)*20)
|
||||
|
||||
var quote rune
|
||||
var dollarQuote string
|
||||
var err error
|
||||
var idx int64
|
||||
max := len(args)
|
||||
escaping := false
|
||||
offset := 0
|
||||
target := query
|
||||
r, sz := utf8.DecodeRuneInString(target)
|
||||
|
||||
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
|
||||
offset += sz
|
||||
target = query[offset:]
|
||||
|
||||
if escaping {
|
||||
escaping = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch r {
|
||||
case '$':
|
||||
if quote != 0 {
|
||||
if quote != '$' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to find the end of dollar quote.
|
||||
pos := offset
|
||||
|
||||
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && r != '$'; r, sz = utf8.DecodeRuneInString(target) {
|
||||
pos += sz
|
||||
target = query[pos:]
|
||||
}
|
||||
|
||||
if sz == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if r == '$' {
|
||||
dq := query[offset : pos+sz]
|
||||
offset = pos
|
||||
target = query[offset:]
|
||||
|
||||
if dq == dollarQuote {
|
||||
quote = 0
|
||||
dollarQuote = ""
|
||||
offset += sz
|
||||
target = query[offset:]
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
oldSz := sz
|
||||
pos := offset
|
||||
r, sz = utf8.DecodeRuneInString(target)
|
||||
|
||||
if '1' <= r && r <= '9' {
|
||||
// A placeholder is found.
|
||||
pos += sz
|
||||
target = query[pos:]
|
||||
|
||||
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && '0' <= r && r <= '9'; r, sz = utf8.DecodeRuneInString(target) {
|
||||
pos += sz
|
||||
target = query[pos:]
|
||||
}
|
||||
|
||||
idx, err = strconv.ParseInt(query[offset:pos], 10, strconv.IntSize)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if int(idx) >= max+1 {
|
||||
return "", ErrInterpolateMissingArgs
|
||||
}
|
||||
|
||||
buf = append(buf, query[:offset-oldSz]...)
|
||||
buf, err = encodeValue(buf, args[idx-1], PostgreSQL)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
query = target
|
||||
offset = 0
|
||||
|
||||
if sz == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to find the beginning of dollar quote.
|
||||
for ; sz != 0 && r != '$' && unicode.IsLetter(r); r, sz = utf8.DecodeRuneInString(target) {
|
||||
pos += sz
|
||||
target = query[pos:]
|
||||
}
|
||||
|
||||
if sz == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if !unicode.IsLetter(r) && r != '$' {
|
||||
continue
|
||||
}
|
||||
|
||||
pos += sz
|
||||
quote = '$'
|
||||
dollarQuote = query[offset:pos]
|
||||
offset = pos
|
||||
target = query[offset:]
|
||||
|
||||
case '\'':
|
||||
if quote == '\'' {
|
||||
quote = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if quote == 0 {
|
||||
quote = '\''
|
||||
}
|
||||
|
||||
case '"':
|
||||
if quote == '"' {
|
||||
quote = 0
|
||||
continue
|
||||
}
|
||||
|
||||
if quote == 0 {
|
||||
quote = '"'
|
||||
}
|
||||
|
||||
case '\\':
|
||||
if quote == '\'' || quote == '"' {
|
||||
escaping = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buf = append(buf, query...)
|
||||
return *(*string)(unsafe.Pointer(&buf)), nil
|
||||
}
|
||||
|
||||
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"...)
|
||||
} else {
|
||||
buf = append(buf, "FALSE"...)
|
||||
}
|
||||
|
||||
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:
|
||||
hex := make([]byte, 0, 2)
|
||||
buf = append(buf, "E'\\\\x"...)
|
||||
|
||||
for _, b := range v {
|
||||
runes := strconv.AppendInt(hex, int64(b), 16)
|
||||
buf = append(buf, byte(unicode.ToUpper(rune(runes[0]))))
|
||||
buf = append(buf, byte(unicode.ToUpper(rune(runes[1]))))
|
||||
}
|
||||
|
||||
buf = append(buf, "'::bytea"...)
|
||||
}
|
||||
|
||||
case string:
|
||||
buf = quoteStringValue(buf, v, flavor)
|
||||
|
||||
case time.Time:
|
||||
if v.IsZero() {
|
||||
buf = append(buf, "'0000-00-00'"...)
|
||||
break
|
||||
}
|
||||
|
||||
// In SQL standard, the precision of fractional seconds in time literal is up to 6 digits.
|
||||
// Round up v.
|
||||
v = v.Add(500 * time.Nanosecond)
|
||||
|
||||
switch flavor {
|
||||
case MySQL:
|
||||
buf = quoteStringValue(buf, v.Format("2006-01-02 15:04:05.999999"), flavor)
|
||||
|
||||
case PostgreSQL:
|
||||
buf = quoteStringValue(buf, v.Format("2006-01-02 15:04:05.999999 MST"), flavor)
|
||||
}
|
||||
|
||||
case fmt.Stringer:
|
||||
buf = quoteStringValue(buf, v.String(), flavor)
|
||||
|
||||
default:
|
||||
return nil, ErrInterpolateUnsupportedArgs
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func quoteStringValue(buf []byte, s string, flavor Flavor) []byte {
|
||||
buf = append(buf, '\'')
|
||||
r, sz := utf8.DecodeRuneInString(s)
|
||||
|
||||
for ; sz != 0; r, sz = utf8.DecodeRuneInString(s) {
|
||||
switch r {
|
||||
case '\x00':
|
||||
buf = append(buf, "\\0"...)
|
||||
|
||||
case '\b':
|
||||
buf = append(buf, "\\b"...)
|
||||
|
||||
case '\n':
|
||||
buf = append(buf, "\\n"...)
|
||||
|
||||
case '\r':
|
||||
buf = append(buf, "\\r"...)
|
||||
|
||||
case '\t':
|
||||
buf = append(buf, "\\t"...)
|
||||
|
||||
case '\x1a':
|
||||
buf = append(buf, "\\Z"...)
|
||||
|
||||
case '\'':
|
||||
buf = append(buf, "\\'"...)
|
||||
|
||||
case '"':
|
||||
buf = append(buf, "\\\""...)
|
||||
|
||||
case '\\':
|
||||
buf = append(buf, "\\\\"...)
|
||||
|
||||
default:
|
||||
buf = append(buf, s[:sz]...)
|
||||
}
|
||||
|
||||
s = s[sz:]
|
||||
}
|
||||
|
||||
buf = append(buf, '\'')
|
||||
return buf
|
||||
}
|
||||
Reference in New Issue
Block a user