Files
zeekay e68f36f2e7 debrand: rename the last interpolator, example and flavor doc
The exported Flavor was already Datastore; this renames the unexported
datastoreInterpolate, the Example function whose suffix still named the vendor,
its ibClick variable, and the flavor list in README. Adds CI running build, vet,
test, gofmt and a grep gate. Zero vendor references remain.
2026-07-26 11:51:15 -07:00

860 lines
18 KiB
Go

// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"database/sql/driver"
"fmt"
"reflect"
"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) {
return mysqlLikeInterpolate(MySQL, query, args...)
}
func mysqlLikeInterpolate(flavor Flavor, 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], flavor)
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 == '\'' {
// PostgreSQL uses two single quotes to represent one single quote.
r, sz = utf8.DecodeRuneInString(target)
if r == '\'' {
offset += sz
target = query[offset:]
continue
}
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
}
// sqlserverInterpolate parses query and replace all "@p*" with encoded args.
// If there are more "@p*" than len(args), returns ErrMissingArgs.
// Otherwise, if there are less "@p*" than len(args), the redundant args are omitted.
func sqlserverInterpolate(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
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 {
continue
}
oldSz := sz
pos := offset
r, sz = utf8.DecodeRuneInString(target)
// Only parameters starting with @p or @P are interpolated.
if r != 'p' && r != 'P' {
continue
}
pos += sz
target = query[pos:]
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+1: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], SQLServer)
if err != nil {
return "", err
}
query = target
offset = 0
if sz == 0 {
break
}
continue
}
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
}
// mysqlInterpolate works the same as MySQL interpolating.
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 datastoreInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Datastore, query, args...)
}
func prestoInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Presto, query, args...)
}
func informixInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Informix, query, args...)
}
func dorisInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Doris, query, args...)
}
// oraclelInterpolate 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 oracleInterpolate(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], Oracle)
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 == '\'' {
// PostgreSQL uses two single quotes to represent one single quote.
r, sz = utf8.DecodeRuneInString(target)
if r == '\'' {
offset += sz
target = query[offset:]
continue
}
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 driver.Valuer:
if val, err := v.Value(); err != nil {
return nil, err
} else {
return encodeValue(buf, val, 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, Datastore, Informix, Doris:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999'")...)
case PostgreSQL:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999 MST'")...)
case SQLite:
buf = append(buf, v.Format("'2006-01-02 15:04:05.000'")...)
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'")...)
case Presto:
buf = append(buf, v.Format("'2006-01-02 15:04:05.000'")...)
case Oracle:
buf = append(buf, "to_timestamp('"...)
buf = append(buf, v.Format("2006-01-02 15:04:05.999999")...)
buf = append(buf, "', 'YYYY-MM-DD HH24:MI:SS.FF')"...)
}
default:
primative := reflect.ValueOf(arg)
// Handle typed nil values (e.g. (*string)(nil), (*time.Time)(nil))
// This check must come before fmt.Stringer check since nil pointers may implement interfaces
if !primative.IsValid() || (primative.Kind() == reflect.Ptr && primative.IsNil()) {
buf = append(buf, "NULL"...)
return buf, nil
}
// Check for fmt.Stringer after nil pointer check
if stringer, ok := arg.(fmt.Stringer); ok {
buf = quoteStringValue(buf, stringer.String(), flavor)
return buf, nil
}
switch k := primative.Kind(); k {
case reflect.Bool:
switch flavor {
case Oracle:
if primative.Bool() {
buf = append(buf, '1')
} else {
buf = append(buf, '0')
}
default:
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, fmt.Errorf("%w: only byte slices are supported", 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 Datastore:
buf = append(buf, "unhex('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
case Presto:
buf = append(buf, "from_hex('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
case Oracle:
buf = append(buf, "hextoraw('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
default:
return nil, fmt.Errorf("%w: unsupported flavor for hex number", ErrInterpolateUnsupportedArgs)
}
default:
return nil, fmt.Errorf("%w: %s", ErrInterpolateUnsupportedArgs, primative.Type().Name())
}
}
return buf, nil
}
var hexDigits = [16]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}
func appendHex(buf, v []byte) []byte {
for _, b := range v {
buf = append(buf, hexDigits[(b>>4)&0xF], hexDigits[b&0xF])
}
return buf
}
func quoteStringValue(buf []byte, s string, flavor Flavor) []byte {
switch flavor {
case PostgreSQL:
buf = append(buf, 'E')
case SQLServer:
buf = append(buf, 'N')
}
buf = append(buf, '\'')
r, sz := utf8.DecodeRuneInString(s)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(s) {
switch r {
case '\x00':
switch flavor {
case SQLite:
buf = append(buf, `'||char(0)||'`...)
default:
buf = append(buf, `\0`...)
}
case '\b':
switch flavor {
case SQLite:
buf = append(buf, '\b')
default:
buf = append(buf, `\b`...)
}
case '\n':
switch flavor {
case SQLite:
buf = append(buf, '\n')
default:
buf = append(buf, `\n`...)
}
case '\r':
switch flavor {
case SQLite:
buf = append(buf, '\r')
default:
buf = append(buf, `\r`...)
}
case '\t':
switch flavor {
case SQLite:
buf = append(buf, '\t')
default:
buf = append(buf, `\t`...)
}
case '\x1a':
switch flavor {
case SQLite:
buf = append(buf, '\x1a')
default:
buf = append(buf, `\Z`...)
}
case '\'':
switch flavor {
case SQLite, CQL:
buf = append(buf, "''"...)
default:
buf = append(buf, `\'`...)
}
case '"':
switch flavor {
case SQLite:
buf = append(buf, '"')
default:
buf = append(buf, `\"`...)
}
case '\\':
switch flavor {
case SQLite:
buf = append(buf, '\\')
default:
buf = append(buf, `\\`...)
}
default:
buf = append(buf, s[:sz]...)
}
s = s[sz:]
}
buf = append(buf, '\'')
return buf
}