mirror of
https://github.com/hanzoai/sqlbuilder.git
synced 2026-10-01 00:37:42 +00:00
- go.mod module path -> github.com/hanzoai/sqlbuilder (package name kept: sqlbuilder) - Rename exported Flavor constant ClickHouse -> Datastore (same iota ordinal 6, behavior byte-identical): const decl, all case labels (Interpolate/Quote/ PrepareInsertIgnore/args/select/union), Flavor.String() literal "ClickHouse" -> "Datastore", and the name<->flavor test map. - Rename unexported dialect helper clickhouseInterpolate -> datastoreInterpolate. - Rename example ExampleInsertBuilder_insertIgnore_clickhouse -> _datastore. - Update tests + README flavor list to Datastore. - Keep upstream MIT LICENSE; add NOTICE recording the fork + rename. Dialect SQL output unchanged; only the identifier is renamed.
438 lines
9.6 KiB
Go
438 lines
9.6 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"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/huandu/go-clone"
|
|
)
|
|
|
|
// Args stores arguments associated with a SQL.
|
|
type Args struct {
|
|
// The default flavor used by `Args#Compile`
|
|
Flavor Flavor
|
|
|
|
indexBase int
|
|
argValues *valueStore
|
|
namedArgs map[string]int
|
|
sqlNamedArgs map[string]int
|
|
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 = 64
|
|
|
|
var predefinedArgs []string
|
|
|
|
// Add adds an arg to Args and returns a placeholder.
|
|
func (args *Args) Add(arg interface{}) string {
|
|
idx := args.add(arg)
|
|
|
|
if idx < maxPredefinedArgs {
|
|
return predefinedArgs[idx]
|
|
}
|
|
|
|
return fmt.Sprintf("$%v", idx)
|
|
}
|
|
|
|
func (args *Args) add(arg interface{}) int {
|
|
idx := args.argValues.Len() + args.indexBase
|
|
|
|
switch a := arg.(type) {
|
|
case sql.NamedArg:
|
|
if args.sqlNamedArgs == nil {
|
|
args.sqlNamedArgs = map[string]int{}
|
|
}
|
|
|
|
if p, ok := args.sqlNamedArgs[a.Name]; ok {
|
|
arg = args.argValues.Load(p)
|
|
break
|
|
}
|
|
|
|
args.sqlNamedArgs[a.Name] = idx
|
|
case namedArgs:
|
|
if args.namedArgs == nil {
|
|
args.namedArgs = map[string]int{}
|
|
}
|
|
|
|
if p, ok := args.namedArgs[a.name]; ok {
|
|
arg = args.argValues.Load(p)
|
|
break
|
|
}
|
|
|
|
// Find out the real arg and add it to args.
|
|
idx = args.add(a.arg)
|
|
args.namedArgs[a.name] = idx
|
|
return idx
|
|
}
|
|
|
|
if args.argValues == nil {
|
|
args.argValues = &valueStore{}
|
|
}
|
|
|
|
args.argValues.Add(arg)
|
|
return idx
|
|
}
|
|
|
|
// Replace replaces the placeholder with arg.
|
|
//
|
|
// The placeholder must be the value returned by `Add`, e.g. "$1".
|
|
// If the placeholder is not found, this method does nothing.
|
|
func (args *Args) Replace(placeholder string, arg interface{}) {
|
|
dollar := strings.IndexRune(placeholder, '$')
|
|
|
|
if dollar != 0 {
|
|
return
|
|
}
|
|
|
|
if i, err := strconv.Atoi(placeholder[1:]); err == nil {
|
|
i -= args.indexBase
|
|
args.argValues.Set(i, arg)
|
|
}
|
|
}
|
|
|
|
// Compile compiles builder's format to standard sql and returns associated args.
|
|
//
|
|
// The format string uses a special syntax to represent arguments.
|
|
//
|
|
// $? refers successive arguments passed in the call. It works similar as `%v` in `fmt.Sprintf`.
|
|
// $0 $1 ... $n refers nth-argument passed in the call. Next $? will use arguments n+1.
|
|
// ${name} refers a named argument created by `Named` with `name`.
|
|
// $$ is a "$" string.
|
|
func (args *Args) Compile(format string, initialValue ...interface{}) (query string, values []interface{}) {
|
|
return args.CompileWithFlavor(format, args.Flavor, initialValue...)
|
|
}
|
|
|
|
// CompileWithFlavor compiles builder's format to standard sql with flavor and returns associated args.
|
|
//
|
|
// See doc for `Compile` to learn details.
|
|
func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue ...interface{}) (query string, values []interface{}) {
|
|
idx := strings.IndexRune(format, '$')
|
|
offset := 0
|
|
ctx := &argsCompileContext{
|
|
stringBuilder: newStringBuilder(),
|
|
Flavor: flavor,
|
|
Values: initialValue,
|
|
}
|
|
|
|
if ctx.Flavor == invalidFlavor {
|
|
ctx.Flavor = DefaultFlavor
|
|
}
|
|
|
|
for idx >= 0 && len(format) > 0 {
|
|
if idx > 0 {
|
|
ctx.WriteString(format[:idx])
|
|
}
|
|
|
|
format = format[idx+1:]
|
|
|
|
// Treat the $ at the end of format is a normal $ rune.
|
|
if len(format) == 0 {
|
|
ctx.WriteRune('$')
|
|
break
|
|
}
|
|
|
|
if r := format[0]; r == '$' {
|
|
ctx.WriteRune('$')
|
|
format = format[1:]
|
|
} else if r == '{' {
|
|
format = args.compileNamed(ctx, format)
|
|
} else if !args.onlyNamed && '0' <= r && r <= '9' {
|
|
format, offset = args.compileDigits(ctx, format, offset)
|
|
} else if !args.onlyNamed && r == '?' {
|
|
format, offset = args.compileSuccessive(ctx, format[1:], offset)
|
|
} else {
|
|
// For unknown $ expression format, treat it as a normal $ rune.
|
|
ctx.WriteRune('$')
|
|
}
|
|
|
|
idx = strings.IndexRune(format, '$')
|
|
}
|
|
|
|
if len(format) > 0 {
|
|
ctx.WriteString(format)
|
|
}
|
|
|
|
query = ctx.String()
|
|
values = args.mergeSQLNamedArgs(ctx)
|
|
return
|
|
}
|
|
|
|
// Value returns the value of the arg.
|
|
// The arg must be the value returned by `Add`.
|
|
func (args *Args) Value(arg string) interface{} {
|
|
_, values := args.Compile(arg)
|
|
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return values[0]
|
|
}
|
|
|
|
func (args *Args) compileNamed(ctx *argsCompileContext, format string) string {
|
|
i := 1
|
|
|
|
for ; i < len(format) && format[i] != '}'; i++ {
|
|
// Nothing.
|
|
}
|
|
|
|
// Invalid $ format. Ignore it.
|
|
if i == len(format) {
|
|
return format
|
|
}
|
|
|
|
name := format[1:i]
|
|
format = format[i+1:]
|
|
|
|
if p, ok := args.namedArgs[name]; ok {
|
|
format, _ = args.compileSuccessive(ctx, format, p-args.indexBase)
|
|
}
|
|
|
|
return format
|
|
}
|
|
|
|
func (args *Args) compileDigits(ctx *argsCompileContext, format string, offset int) (string, int) {
|
|
i := 1
|
|
|
|
for ; i < len(format) && '0' <= format[i] && format[i] <= '9'; i++ {
|
|
// Nothing.
|
|
}
|
|
|
|
digits := format[:i]
|
|
format = format[i:]
|
|
|
|
if pointer, err := strconv.Atoi(digits); err == nil {
|
|
return args.compileSuccessive(ctx, format, pointer-args.indexBase)
|
|
}
|
|
|
|
return format, offset
|
|
}
|
|
|
|
func (args *Args) compileSuccessive(ctx *argsCompileContext, format string, offset int) (string, int) {
|
|
if offset < 0 || offset >= args.argValues.Len() {
|
|
ctx.WriteString("/* INVALID ARG $")
|
|
ctx.WriteString(strconv.Itoa(offset))
|
|
ctx.WriteString(" */")
|
|
return format, offset
|
|
}
|
|
|
|
arg := args.argValues.Load(offset)
|
|
ctx.WriteValue(arg)
|
|
|
|
return format, offset + 1
|
|
}
|
|
|
|
func (args *Args) mergeSQLNamedArgs(ctx *argsCompileContext) []interface{} {
|
|
if len(args.sqlNamedArgs) == 0 && len(ctx.NamedArgs) == 0 {
|
|
return ctx.Values
|
|
}
|
|
|
|
values := ctx.Values
|
|
existingNames := make(map[string]struct{}, len(ctx.NamedArgs))
|
|
|
|
// Add all named args to values.
|
|
// Remove duplicated named args in this step.
|
|
for _, arg := range ctx.NamedArgs {
|
|
if _, ok := existingNames[arg.Name]; !ok {
|
|
existingNames[arg.Name] = struct{}{}
|
|
values = append(values, arg)
|
|
}
|
|
}
|
|
|
|
// Stabilize the sequence to make it easier to write test cases.
|
|
ints := make([]int, 0, len(args.sqlNamedArgs))
|
|
|
|
for n, p := range args.sqlNamedArgs {
|
|
if _, ok := existingNames[n]; ok {
|
|
continue
|
|
}
|
|
|
|
ints = append(ints, p)
|
|
}
|
|
|
|
sort.Ints(ints)
|
|
|
|
for _, i := range ints {
|
|
values = append(values, args.argValues.Load(i))
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func parseNamedArgs(initialValue []interface{}) (values []interface{}, namedValues []sql.NamedArg) {
|
|
if len(initialValue) == 0 {
|
|
values = initialValue
|
|
return
|
|
}
|
|
|
|
// sql.NamedArgs must be placed at the end of the initial value.
|
|
size := len(initialValue)
|
|
i := size
|
|
|
|
for ; i > 0; i-- {
|
|
switch initialValue[i-1].(type) {
|
|
case sql.NamedArg:
|
|
continue
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if i == size {
|
|
values = initialValue
|
|
return
|
|
}
|
|
|
|
values = initialValue[:i]
|
|
namedValues = make([]sql.NamedArg, 0, size-i)
|
|
|
|
for ; i < size; i++ {
|
|
namedValues = append(namedValues, initialValue[i].(sql.NamedArg))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type argsCompileContext struct {
|
|
*stringBuilder
|
|
|
|
Flavor Flavor
|
|
Values []interface{}
|
|
NamedArgs []sql.NamedArg
|
|
}
|
|
|
|
func (ctx *argsCompileContext) WriteValue(arg interface{}) {
|
|
switch a := arg.(type) {
|
|
case Builder:
|
|
s, values := a.BuildWithFlavor(ctx.Flavor, ctx.Values...)
|
|
ctx.WriteString(s)
|
|
|
|
// Add all values to ctx.
|
|
// Named args must be located at the end of values.
|
|
values, namedArgs := parseNamedArgs(values)
|
|
ctx.Values = values
|
|
ctx.NamedArgs = append(ctx.NamedArgs, namedArgs...)
|
|
|
|
case sql.NamedArg:
|
|
ctx.WriteRune('@')
|
|
ctx.WriteString(a.Name)
|
|
ctx.NamedArgs = append(ctx.NamedArgs, a)
|
|
|
|
case rawArgs:
|
|
ctx.WriteString(a.expr)
|
|
|
|
case listArgs:
|
|
if a.isTuple {
|
|
ctx.WriteRune('(')
|
|
}
|
|
|
|
if len(a.args) > 0 {
|
|
ctx.WriteValue(a.args[0])
|
|
}
|
|
|
|
for i := 1; i < len(a.args); i++ {
|
|
ctx.WriteString(", ")
|
|
ctx.WriteValue(a.args[i])
|
|
}
|
|
|
|
if a.isTuple {
|
|
ctx.WriteRune(')')
|
|
}
|
|
|
|
case condBuilder:
|
|
a.Builder(ctx)
|
|
|
|
default:
|
|
switch ctx.Flavor {
|
|
case MySQL, SQLite, CQL, Datastore, Presto, Informix, Doris:
|
|
ctx.WriteRune('?')
|
|
case PostgreSQL:
|
|
fmt.Fprintf(ctx, "$%d", len(ctx.Values)+1)
|
|
case SQLServer:
|
|
fmt.Fprintf(ctx, "@p%d", len(ctx.Values)+1)
|
|
case Oracle:
|
|
fmt.Fprintf(ctx, ":%d", len(ctx.Values)+1)
|
|
default:
|
|
panic(fmt.Errorf("Args.CompileWithFlavor: invalid flavor %v (%v)", ctx.Flavor, int(ctx.Flavor)))
|
|
}
|
|
|
|
ctx.Values = append(ctx.Values, arg)
|
|
}
|
|
}
|
|
|
|
func (ctx *argsCompileContext) WriteValues(values []interface{}, sep string) {
|
|
if len(values) == 0 {
|
|
return
|
|
}
|
|
|
|
ctx.WriteValue(values[0])
|
|
|
|
for _, v := range values[1:] {
|
|
ctx.WriteString(sep)
|
|
ctx.WriteValue(v)
|
|
}
|
|
}
|
|
|
|
type valueStore struct {
|
|
Values []interface{}
|
|
}
|
|
|
|
func init() {
|
|
// The values in valueStore should be shadow-copied to avoid unnecessary cost.
|
|
t := reflect.TypeOf(valueStore{})
|
|
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
|
|
values := old.FieldByName("Values")
|
|
newValues := allocator.Clone(values)
|
|
new.FieldByName("Values").Set(newValues)
|
|
})
|
|
}
|
|
|
|
func (as *valueStore) Len() int {
|
|
if as == nil {
|
|
return 0
|
|
}
|
|
|
|
return len(as.Values)
|
|
}
|
|
|
|
// Add adds an arg to argsValues and returns its index.
|
|
func (as *valueStore) Add(arg interface{}) int {
|
|
as.Values = append(as.Values, arg)
|
|
return len(as.Values) - 1
|
|
}
|
|
|
|
// Set sets the arg value by index.
|
|
func (as *valueStore) Set(index int, arg interface{}) {
|
|
if as == nil || index < 0 || index >= len(as.Values) {
|
|
return
|
|
}
|
|
|
|
as.Values[index] = arg
|
|
}
|
|
|
|
// Load returns the arg value by index.
|
|
// Returns nil if index is out of range or as itself is nil.
|
|
func (as *valueStore) Load(index int) interface{} {
|
|
if as == nil || index < 0 || index >= len(as.Values) {
|
|
return nil
|
|
}
|
|
|
|
return as.Values[index]
|
|
}
|