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.
392 lines
9.7 KiB
Go
392 lines
9.7 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 (
|
|
"fmt"
|
|
"reflect"
|
|
|
|
"github.com/huandu/go-clone"
|
|
)
|
|
|
|
const (
|
|
unionDistinct = " UNION " // Default union type is DISTINCT.
|
|
unionAll = " UNION ALL "
|
|
)
|
|
|
|
const (
|
|
unionMarkerInit injectionMarker = iota
|
|
unionMarkerAfterUnion
|
|
unionMarkerAfterOrderBy
|
|
unionMarkerAfterLimit
|
|
)
|
|
|
|
// NewUnionBuilder creates a new UNION builder.
|
|
func NewUnionBuilder() *UnionBuilder {
|
|
return DefaultFlavor.NewUnionBuilder()
|
|
}
|
|
|
|
func newUnionBuilder() *UnionBuilder {
|
|
return &UnionBuilder{
|
|
args: &Args{},
|
|
injection: newInjection(),
|
|
}
|
|
}
|
|
|
|
// Clone returns a deep copy of UnionBuilder.
|
|
// It's useful when you want to create a base builder and clone it to build similar queries.
|
|
func (ub *UnionBuilder) Clone() *UnionBuilder {
|
|
return clone.Clone(ub).(*UnionBuilder)
|
|
}
|
|
|
|
func init() {
|
|
t := reflect.TypeOf(UnionBuilder{})
|
|
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
|
|
cloned := allocator.CloneSlowly(old)
|
|
new.Set(cloned)
|
|
|
|
ub := cloned.Addr().Interface().(*UnionBuilder)
|
|
for i, b := range ub.builders {
|
|
ub.args.Replace(ub.builderVars[i], b)
|
|
}
|
|
})
|
|
}
|
|
|
|
// UnionBuilder is a builder to build UNION.
|
|
type UnionBuilder struct {
|
|
opt string
|
|
orderByCols []string
|
|
order string
|
|
limitVar string
|
|
offsetVar string
|
|
|
|
builders []Builder
|
|
builderVars []string
|
|
|
|
args *Args
|
|
|
|
injection *injection
|
|
marker injectionMarker
|
|
}
|
|
|
|
var _ Builder = new(UnionBuilder)
|
|
|
|
// Union unions all builders together using UNION operator.
|
|
func Union(builders ...Builder) *UnionBuilder {
|
|
return DefaultFlavor.NewUnionBuilder().Union(builders...)
|
|
}
|
|
|
|
// Union unions all builders together using UNION operator.
|
|
func (ub *UnionBuilder) Union(builders ...Builder) *UnionBuilder {
|
|
return ub.union(unionDistinct, builders...)
|
|
}
|
|
|
|
// UnionAll unions all builders together using UNION ALL operator.
|
|
func UnionAll(builders ...Builder) *UnionBuilder {
|
|
return DefaultFlavor.NewUnionBuilder().UnionAll(builders...)
|
|
}
|
|
|
|
// UnionAll unions all builders together using UNION ALL operator.
|
|
func (ub *UnionBuilder) UnionAll(builders ...Builder) *UnionBuilder {
|
|
return ub.union(unionAll, builders...)
|
|
}
|
|
|
|
func (ub *UnionBuilder) union(opt string, builders ...Builder) *UnionBuilder {
|
|
builderVars := make([]string, 0, len(builders))
|
|
|
|
for _, b := range builders {
|
|
builderVars = append(builderVars, ub.Var(b))
|
|
}
|
|
|
|
ub.opt = opt
|
|
ub.builders = append([]Builder(nil), builders...)
|
|
ub.builderVars = builderVars
|
|
ub.marker = unionMarkerAfterUnion
|
|
return ub
|
|
}
|
|
|
|
// OrderBy sets columns of ORDER BY in SELECT.
|
|
//
|
|
// It's recommended to use OrderByAsc or OrderByDesc instead for better support of multiple ORDER BY columns with different directions.
|
|
// OrderBy combined with Asc/Desc only supports a single direction for all columns.
|
|
func (ub *UnionBuilder) OrderBy(col ...string) *UnionBuilder {
|
|
ub.orderByCols = col
|
|
ub.marker = unionMarkerAfterOrderBy
|
|
return ub
|
|
}
|
|
|
|
// OrderByAsc sets a column of ORDER BY in SELECT with ASC order.
|
|
// It supports chaining multiple calls to add multiple ORDER BY columns with different directions.
|
|
//
|
|
// ub.OrderByAsc("name").OrderByDesc("id")
|
|
// // Generates: ORDER BY name ASC, id DESC
|
|
func (ub *UnionBuilder) OrderByAsc(col string) *UnionBuilder {
|
|
ub.orderByCols = append(ub.orderByCols, col+" ASC")
|
|
ub.marker = unionMarkerAfterOrderBy
|
|
return ub
|
|
}
|
|
|
|
// OrderByDesc sets a column of ORDER BY in SELECT with DESC order.
|
|
// It supports chaining multiple calls to add multiple ORDER BY columns with different directions.
|
|
//
|
|
// ub.OrderByDesc("id").OrderByAsc("name")
|
|
// // Generates: ORDER BY id DESC, name ASC
|
|
func (ub *UnionBuilder) OrderByDesc(col string) *UnionBuilder {
|
|
ub.orderByCols = append(ub.orderByCols, col+" DESC")
|
|
ub.marker = unionMarkerAfterOrderBy
|
|
return ub
|
|
}
|
|
|
|
// Asc sets order of ORDER BY to ASC.
|
|
//
|
|
// Deprecated: Use OrderByAsc instead. Asc only supports a single direction for all ORDER BY columns.
|
|
func (ub *UnionBuilder) Asc() *UnionBuilder {
|
|
ub.order = "ASC"
|
|
ub.marker = unionMarkerAfterOrderBy
|
|
return ub
|
|
}
|
|
|
|
// Desc sets order of ORDER BY to DESC.
|
|
//
|
|
// Deprecated: Use OrderByDesc instead. Desc only supports a single direction for all ORDER BY columns.
|
|
func (ub *UnionBuilder) Desc() *UnionBuilder {
|
|
ub.order = "DESC"
|
|
ub.marker = unionMarkerAfterOrderBy
|
|
return ub
|
|
}
|
|
|
|
// Limit sets the LIMIT in SELECT.
|
|
func (ub *UnionBuilder) Limit(limit int) *UnionBuilder {
|
|
if limit < 0 {
|
|
ub.limitVar = ""
|
|
return ub
|
|
}
|
|
|
|
ub.limitVar = ub.Var(limit)
|
|
ub.marker = unionMarkerAfterLimit
|
|
return ub
|
|
}
|
|
|
|
// Offset sets the LIMIT offset in SELECT.
|
|
func (ub *UnionBuilder) Offset(offset int) *UnionBuilder {
|
|
if offset < 0 {
|
|
ub.offsetVar = ""
|
|
return ub
|
|
}
|
|
|
|
ub.offsetVar = ub.Var(offset)
|
|
ub.marker = unionMarkerAfterLimit
|
|
return ub
|
|
}
|
|
|
|
// String returns the compiled SELECT string.
|
|
func (ub *UnionBuilder) String() string {
|
|
s, _ := ub.Build()
|
|
return s
|
|
}
|
|
|
|
// Build returns compiled SELECT string and args.
|
|
// They can be used in `DB#Query` of package `database/sql` directly.
|
|
func (ub *UnionBuilder) Build() (sql string, args []interface{}) {
|
|
return ub.BuildWithFlavor(ub.args.Flavor)
|
|
}
|
|
|
|
// BuildWithFlavor returns compiled SELECT string and args with flavor and initial args.
|
|
// They can be used in `DB#Query` of package `database/sql` directly.
|
|
func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
|
|
buf := newStringBuilder()
|
|
ub.injection.WriteTo(buf, unionMarkerInit)
|
|
|
|
nestedSelect := (flavor == Oracle && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0)) ||
|
|
(flavor == Informix && len(ub.limitVar) > 0)
|
|
|
|
if len(ub.builderVars) > 0 {
|
|
needParen := flavor != SQLite
|
|
|
|
if nestedSelect {
|
|
buf.WriteLeadingString("SELECT * FROM (")
|
|
}
|
|
|
|
if needParen {
|
|
buf.WriteLeadingString("(")
|
|
buf.WriteString(ub.builderVars[0])
|
|
buf.WriteRune(')')
|
|
} else {
|
|
buf.WriteLeadingString(ub.builderVars[0])
|
|
}
|
|
|
|
for _, b := range ub.builderVars[1:] {
|
|
buf.WriteString(ub.opt)
|
|
|
|
if needParen {
|
|
buf.WriteRune('(')
|
|
}
|
|
|
|
buf.WriteString(b)
|
|
|
|
if needParen {
|
|
buf.WriteRune(')')
|
|
}
|
|
}
|
|
|
|
if nestedSelect {
|
|
buf.WriteLeadingString(")")
|
|
}
|
|
}
|
|
|
|
ub.injection.WriteTo(buf, unionMarkerAfterUnion)
|
|
|
|
if len(ub.orderByCols) > 0 {
|
|
buf.WriteLeadingString("ORDER BY ")
|
|
buf.WriteStrings(ub.orderByCols, ", ")
|
|
|
|
if ub.order != "" {
|
|
buf.WriteRune(' ')
|
|
buf.WriteString(ub.order)
|
|
}
|
|
|
|
ub.injection.WriteTo(buf, unionMarkerAfterOrderBy)
|
|
}
|
|
|
|
switch flavor {
|
|
case MySQL, SQLite, Datastore:
|
|
if len(ub.limitVar) > 0 {
|
|
buf.WriteLeadingString("LIMIT ")
|
|
buf.WriteString(ub.limitVar)
|
|
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(ub.offsetVar)
|
|
}
|
|
}
|
|
|
|
case CQL:
|
|
if len(ub.limitVar) > 0 {
|
|
buf.WriteLeadingString("LIMIT ")
|
|
buf.WriteString(ub.limitVar)
|
|
}
|
|
|
|
case PostgreSQL:
|
|
if len(ub.limitVar) > 0 {
|
|
buf.WriteLeadingString("LIMIT ")
|
|
buf.WriteString(ub.limitVar)
|
|
}
|
|
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(ub.offsetVar)
|
|
}
|
|
|
|
case Presto:
|
|
// There might be a hidden constraint in Presto requiring offset to be set before limit.
|
|
// The select statement documentation (https://prestodb.io/docs/current/sql/select.html)
|
|
// puts offset before limit, and Trino, which is based on Presto, seems
|
|
// to require this specific order.
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(ub.offsetVar)
|
|
}
|
|
|
|
if len(ub.limitVar) > 0 {
|
|
buf.WriteLeadingString("LIMIT ")
|
|
buf.WriteString(ub.limitVar)
|
|
}
|
|
|
|
case SQLServer:
|
|
// If ORDER BY is not set, sort column #1 by default.
|
|
// It's required to make OFFSET...FETCH work.
|
|
if len(ub.orderByCols) == 0 && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0) {
|
|
buf.WriteLeadingString("ORDER BY 1")
|
|
}
|
|
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(ub.offsetVar)
|
|
buf.WriteString(" ROWS")
|
|
}
|
|
|
|
if len(ub.limitVar) > 0 {
|
|
if len(ub.offsetVar) == 0 {
|
|
buf.WriteLeadingString("OFFSET 0 ROWS")
|
|
}
|
|
|
|
buf.WriteLeadingString("FETCH NEXT ")
|
|
buf.WriteString(ub.limitVar)
|
|
buf.WriteString(" ROWS ONLY")
|
|
}
|
|
|
|
case Oracle:
|
|
// It's required to make OFFSET...FETCH work.
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(ub.offsetVar)
|
|
buf.WriteString(" ROWS")
|
|
}
|
|
|
|
if len(ub.limitVar) > 0 {
|
|
if len(ub.offsetVar) == 0 {
|
|
buf.WriteLeadingString("OFFSET 0 ROWS")
|
|
}
|
|
|
|
buf.WriteLeadingString("FETCH NEXT ")
|
|
buf.WriteString(ub.limitVar)
|
|
buf.WriteString(" ROWS ONLY")
|
|
}
|
|
|
|
case Informix:
|
|
// [SKIP N] FIRST M
|
|
// M must be greater than 0
|
|
if len(ub.limitVar) > 0 {
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("SKIP ")
|
|
buf.WriteString(ub.offsetVar)
|
|
}
|
|
|
|
buf.WriteLeadingString("FIRST ")
|
|
buf.WriteString(ub.limitVar)
|
|
}
|
|
|
|
case Doris:
|
|
// #192: Doris doesn't support ? in OFFSET and LIMIT.
|
|
if len(ub.limitVar) > 0 {
|
|
buf.WriteLeadingString("LIMIT ")
|
|
buf.WriteString(fmt.Sprint(ub.args.Value(ub.limitVar)))
|
|
|
|
if len(ub.offsetVar) > 0 {
|
|
buf.WriteLeadingString("OFFSET ")
|
|
buf.WriteString(fmt.Sprint(ub.args.Value(ub.offsetVar)))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(ub.limitVar) > 0 {
|
|
ub.injection.WriteTo(buf, unionMarkerAfterLimit)
|
|
}
|
|
|
|
return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
|
|
}
|
|
|
|
// SetFlavor sets the flavor of compiled sql.
|
|
func (ub *UnionBuilder) SetFlavor(flavor Flavor) (old Flavor) {
|
|
old = ub.args.Flavor
|
|
ub.args.Flavor = flavor
|
|
return
|
|
}
|
|
|
|
// Flavor returns flavor of builder
|
|
func (ub *UnionBuilder) Flavor() Flavor {
|
|
return ub.args.Flavor
|
|
}
|
|
|
|
// Var returns a placeholder for value.
|
|
func (ub *UnionBuilder) Var(arg interface{}) string {
|
|
return ub.args.Add(arg)
|
|
}
|
|
|
|
// SQL adds an arbitrary sql to current position.
|
|
func (ub *UnionBuilder) SQL(sql string) *UnionBuilder {
|
|
ub.injection.SQL(ub.marker, sql)
|
|
return ub
|
|
}
|