fork github.com/doug-martin
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type baseSQLGeneratorSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (bsgs *baseSQLGeneratorSuite) assertNotPreparedSQL(b sb.SQLBuilder, expectedSQL string) {
|
||||
actualSQL, actualArgs, err := b.ToSQL()
|
||||
bsgs.NoError(err)
|
||||
bsgs.Equal(expectedSQL, actualSQL)
|
||||
bsgs.Empty(actualArgs)
|
||||
}
|
||||
|
||||
func (bsgs *baseSQLGeneratorSuite) assertPreparedSQL(
|
||||
b sb.SQLBuilder,
|
||||
expectedSQL string,
|
||||
expectedArgs []interface{},
|
||||
) {
|
||||
actualSQL, actualArgs, err := b.ToSQL()
|
||||
bsgs.NoError(err)
|
||||
bsgs.Equal(expectedSQL, actualSQL)
|
||||
if len(actualArgs) == 0 {
|
||||
bsgs.Empty(expectedArgs)
|
||||
} else {
|
||||
bsgs.Equal(expectedArgs, actualArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func (bsgs *baseSQLGeneratorSuite) assertErrorSQL(b sb.SQLBuilder, errMsg string) {
|
||||
actualSQL, actualArgs, err := b.ToSQL()
|
||||
bsgs.EqualError(err, errMsg)
|
||||
bsgs.Empty(actualSQL)
|
||||
bsgs.Empty(actualArgs)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
var ErrNoUpdatedValuesProvided = errors.New("no update values provided")
|
||||
|
||||
func ErrCTENotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support CTE WITH clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func ErrRecursiveCTENotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support CTE WITH RECURSIVE clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func ErrReturnNotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support RETURNING clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func ErrNotSupportedFragment(sqlType string, f SQLFragmentType) error {
|
||||
return errors.New("unsupported %s SQL fragment %s", sqlType, f)
|
||||
}
|
||||
|
||||
type (
|
||||
CommonSQLGenerator interface {
|
||||
Dialect() string
|
||||
DialectOptions() *SQLDialectOptions
|
||||
ExpressionSQLGenerator() ExpressionSQLGenerator
|
||||
ReturningSQL(b sb.SQLBuilder, returns exp.ColumnListExpression)
|
||||
FromSQL(b sb.SQLBuilder, from exp.ColumnListExpression)
|
||||
SourcesSQL(b sb.SQLBuilder, from exp.ColumnListExpression)
|
||||
WhereSQL(b sb.SQLBuilder, where exp.ExpressionList)
|
||||
OrderSQL(b sb.SQLBuilder, order exp.ColumnListExpression)
|
||||
OrderWithOffsetFetchSQL(b sb.SQLBuilder, order exp.ColumnListExpression, offset uint, limit interface{})
|
||||
LimitSQL(b sb.SQLBuilder, limit interface{})
|
||||
UpdateExpressionSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression)
|
||||
}
|
||||
commonSQLGenerator struct {
|
||||
dialect string
|
||||
esg ExpressionSQLGenerator
|
||||
dialectOptions *SQLDialectOptions
|
||||
}
|
||||
)
|
||||
|
||||
func NewCommonSQLGenerator(dialect string, do *SQLDialectOptions) CommonSQLGenerator {
|
||||
return &commonSQLGenerator{dialect: dialect, esg: NewExpressionSQLGenerator(dialect, do), dialectOptions: do}
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) Dialect() string {
|
||||
return csg.dialect
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) DialectOptions() *SQLDialectOptions {
|
||||
return csg.dialectOptions
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) ExpressionSQLGenerator() ExpressionSQLGenerator {
|
||||
return csg.esg
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) ReturningSQL(b sb.SQLBuilder, returns exp.ColumnListExpression) {
|
||||
if returns != nil && len(returns.Columns()) > 0 {
|
||||
if csg.dialectOptions.SupportsReturn {
|
||||
b.Write(csg.dialectOptions.ReturningFragment)
|
||||
csg.esg.Generate(b, returns)
|
||||
} else {
|
||||
b.SetError(ErrReturnNotSupported(csg.dialect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the FROM clause and tables to an sql statement
|
||||
func (csg *commonSQLGenerator) FromSQL(b sb.SQLBuilder, from exp.ColumnListExpression) {
|
||||
if from != nil && !from.IsEmpty() {
|
||||
b.Write(csg.dialectOptions.FromFragment)
|
||||
csg.SourcesSQL(b, from)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the generates the SQL for a column list
|
||||
func (csg *commonSQLGenerator) SourcesSQL(b sb.SQLBuilder, from exp.ColumnListExpression) {
|
||||
b.WriteRunes(csg.dialectOptions.SpaceRune)
|
||||
csg.esg.Generate(b, from)
|
||||
}
|
||||
|
||||
// Generates the WHERE clause for an SQL statement
|
||||
func (csg *commonSQLGenerator) WhereSQL(b sb.SQLBuilder, where exp.ExpressionList) {
|
||||
if where != nil && !where.IsEmpty() {
|
||||
b.Write(csg.dialectOptions.WhereFragment)
|
||||
csg.esg.Generate(b, where)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the ORDER BY clause for an SQL statement
|
||||
func (csg *commonSQLGenerator) OrderSQL(b sb.SQLBuilder, order exp.ColumnListExpression) {
|
||||
if order != nil && len(order.Columns()) > 0 {
|
||||
b.Write(csg.dialectOptions.OrderByFragment)
|
||||
csg.esg.Generate(b, order)
|
||||
}
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) OrderWithOffsetFetchSQL(
|
||||
b sb.SQLBuilder,
|
||||
order exp.ColumnListExpression,
|
||||
offset uint,
|
||||
limit interface{},
|
||||
) {
|
||||
if order == nil {
|
||||
return
|
||||
}
|
||||
|
||||
csg.OrderSQL(b, order)
|
||||
if offset > 0 {
|
||||
b.Write(csg.dialectOptions.OffsetFragment)
|
||||
csg.esg.Generate(b, offset)
|
||||
b.Write([]byte(" ROWS"))
|
||||
|
||||
if limit != nil {
|
||||
b.Write(csg.dialectOptions.FetchFragment)
|
||||
csg.esg.Generate(b, limit)
|
||||
b.Write([]byte(" ROWS ONLY"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the LIMIT clause for an SQL statement
|
||||
func (csg *commonSQLGenerator) LimitSQL(b sb.SQLBuilder, limit interface{}) {
|
||||
if limit != nil {
|
||||
b.Write(csg.dialectOptions.LimitFragment)
|
||||
if csg.dialectOptions.SurroundLimitWithParentheses {
|
||||
b.WriteRunes(csg.dialectOptions.LeftParenRune)
|
||||
}
|
||||
csg.esg.Generate(b, limit)
|
||||
if csg.dialectOptions.SurroundLimitWithParentheses {
|
||||
b.WriteRunes(csg.dialectOptions.RightParenRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (csg *commonSQLGenerator) UpdateExpressionSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression) {
|
||||
if len(updates) == 0 {
|
||||
b.SetError(ErrNoUpdatedValuesProvided)
|
||||
return
|
||||
}
|
||||
updateLen := len(updates)
|
||||
for i, update := range updates {
|
||||
csg.esg.Generate(b, update)
|
||||
if i < updateLen-1 {
|
||||
b.WriteRunes(csg.dialectOptions.CommaRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
commonSQLTestCase struct {
|
||||
gen func(builder sb.SQLBuilder)
|
||||
sql string
|
||||
isPrepared bool
|
||||
err string
|
||||
args []interface{}
|
||||
}
|
||||
commonSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) assertCases(testCases ...commonSQLTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
tc.gen(b)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
csgs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
csgs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
csgs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestReturningSQL() {
|
||||
returningGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.ReturningSQL(sb, exp.NewColumnListExpression("a", "b"))
|
||||
}
|
||||
}
|
||||
|
||||
returningNoColsGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.ReturningSQL(sb, exp.NewColumnListExpression())
|
||||
}
|
||||
}
|
||||
|
||||
returningNilExpGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.ReturningSQL(sb, nil)
|
||||
}
|
||||
}
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsReturn = true
|
||||
csgs1 := sqlgen.NewCommonSQLGenerator("test", opts)
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
opts2.SupportsReturn = false
|
||||
csgs2 := sqlgen.NewCommonSQLGenerator("test", opts2)
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: returningGen(csgs1), sql: ` RETURNING "a", "b"`},
|
||||
commonSQLTestCase{gen: returningGen(csgs1), sql: ` RETURNING "a", "b"`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: returningNoColsGen(csgs1), sql: ``},
|
||||
commonSQLTestCase{gen: returningNoColsGen(csgs1), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: returningNilExpGen(csgs1), sql: ``},
|
||||
commonSQLTestCase{gen: returningNilExpGen(csgs1), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: returningGen(csgs2), err: `db: dialect does not support RETURNING clause [dialect=test]`},
|
||||
commonSQLTestCase{gen: returningGen(csgs2), err: `db: dialect does not support RETURNING clause [dialect=test]`},
|
||||
)
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestFromSQL() {
|
||||
fromGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.FromSQL(sb, exp.NewColumnListExpression("a", "b"))
|
||||
}
|
||||
}
|
||||
|
||||
fromNoColsGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.FromSQL(sb, exp.NewColumnListExpression())
|
||||
}
|
||||
}
|
||||
|
||||
fromNilExpGen := func(csgs sqlgen.CommonSQLGenerator) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.FromSQL(sb, nil)
|
||||
}
|
||||
}
|
||||
|
||||
csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions())
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.FromFragment = []byte(" from")
|
||||
csgFromFrag := sqlgen.NewCommonSQLGenerator("test", opts)
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: fromGen(csg), sql: ` FROM "a", "b"`},
|
||||
commonSQLTestCase{gen: fromGen(csg), sql: ` FROM "a", "b"`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: fromNoColsGen(csg), sql: ``},
|
||||
commonSQLTestCase{gen: fromNoColsGen(csg), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: fromNilExpGen(csg), sql: ``},
|
||||
commonSQLTestCase{gen: fromNilExpGen(csg), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: fromGen(csgFromFrag), sql: ` from "a", "b"`},
|
||||
commonSQLTestCase{gen: fromGen(csgFromFrag), sql: ` from "a", "b"`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: fromNoColsGen(csgFromFrag), sql: ``},
|
||||
commonSQLTestCase{gen: fromNoColsGen(csgFromFrag), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: fromNilExpGen(csgFromFrag), sql: ``},
|
||||
commonSQLTestCase{gen: fromNilExpGen(csgFromFrag), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
)
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestWhereSQL() {
|
||||
whereAndGen := func(csgs sqlgen.CommonSQLGenerator, exps ...exp.Expression) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.WhereSQL(sb, exp.NewExpressionList(exp.AndType, exps...))
|
||||
}
|
||||
}
|
||||
|
||||
whereOrGen := func(csgs sqlgen.CommonSQLGenerator, exps ...exp.Expression) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.WhereSQL(sb, exp.NewExpressionList(exp.OrType, exps...))
|
||||
}
|
||||
}
|
||||
|
||||
csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions())
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.WhereFragment = []byte(" where ")
|
||||
csgWhereFrag := sqlgen.NewCommonSQLGenerator("test", opts)
|
||||
|
||||
w := exp.Ex{"a": "b"}
|
||||
w2 := exp.Ex{"b": "c"}
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: whereAndGen(csg), sql: ``},
|
||||
commonSQLTestCase{gen: whereAndGen(csg), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: whereAndGen(csg, w), sql: ` WHERE ("a" = 'b')`},
|
||||
commonSQLTestCase{gen: whereAndGen(csg, w), sql: ` WHERE ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
commonSQLTestCase{gen: whereAndGen(csg, w, w2), sql: ` WHERE (("a" = 'b') AND ("b" = 'c'))`},
|
||||
commonSQLTestCase{gen: whereAndGen(csg, w, w2), sql: ` WHERE (("a" = ?) AND ("b" = ?))`, isPrepared: true, args: []interface{}{"b", "c"}},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csg), sql: ``},
|
||||
commonSQLTestCase{gen: whereOrGen(csg), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csg, w), sql: ` WHERE ("a" = 'b')`},
|
||||
commonSQLTestCase{gen: whereOrGen(csg, w), sql: ` WHERE ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csg, w, w2), sql: ` WHERE (("a" = 'b') OR ("b" = 'c'))`},
|
||||
commonSQLTestCase{gen: whereOrGen(csg, w, w2), sql: ` WHERE (("a" = ?) OR ("b" = ?))`, isPrepared: true, args: []interface{}{"b", "c"}},
|
||||
|
||||
commonSQLTestCase{gen: whereAndGen(csgWhereFrag), sql: ``},
|
||||
commonSQLTestCase{gen: whereAndGen(csgWhereFrag), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w), sql: ` where ("a" = 'b')`},
|
||||
commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w), sql: ` where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
commonSQLTestCase{gen: whereAndGen(csgWhereFrag, w, w2), sql: ` where (("a" = 'b') AND ("b" = 'c'))`},
|
||||
commonSQLTestCase{
|
||||
gen: whereAndGen(csgWhereFrag, w, w2),
|
||||
sql: ` where (("a" = ?) AND ("b" = ?))`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csgWhereFrag), sql: ``},
|
||||
commonSQLTestCase{gen: whereOrGen(csgWhereFrag), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w), sql: ` where ("a" = 'b')`},
|
||||
commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w), sql: ` where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
commonSQLTestCase{gen: whereOrGen(csgWhereFrag, w, w2), sql: ` where (("a" = 'b') OR ("b" = 'c'))`},
|
||||
commonSQLTestCase{
|
||||
gen: whereOrGen(csgWhereFrag, w, w2),
|
||||
sql: ` where (("a" = ?) OR ("b" = ?))`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestOrderSQL() {
|
||||
orderGen := func(csgs sqlgen.CommonSQLGenerator, o ...exp.OrderedExpression) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.OrderSQL(sb, exp.NewOrderedColumnList(o...))
|
||||
}
|
||||
}
|
||||
|
||||
csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions())
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// override fragments to ensure they are used
|
||||
opts.OrderByFragment = []byte(" order by ")
|
||||
opts.AscFragment = []byte(" asc")
|
||||
opts.DescFragment = []byte(" desc")
|
||||
opts.NullsFirstFragment = []byte(" nulls first")
|
||||
opts.NullsLastFragment = []byte(" nulls last")
|
||||
csgCustom := sqlgen.NewCommonSQLGenerator("test", opts)
|
||||
|
||||
ident := exp.NewIdentifierExpression("", "", "a")
|
||||
oa := ident.Asc()
|
||||
oanf := ident.Asc().NullsFirst()
|
||||
oanl := ident.Asc().NullsLast()
|
||||
|
||||
od := ident.Desc()
|
||||
odnf := ident.Desc().NullsFirst()
|
||||
odnl := ident.Desc().NullsLast()
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: orderGen(csg), sql: ``},
|
||||
commonSQLTestCase{gen: orderGen(csg), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, oa), sql: ` ORDER BY "a" ASC`},
|
||||
commonSQLTestCase{gen: orderGen(csg, oa), sql: ` ORDER BY "a" ASC`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, oanf), sql: ` ORDER BY "a" ASC NULLS FIRST`},
|
||||
commonSQLTestCase{gen: orderGen(csg, oanf), sql: ` ORDER BY "a" ASC NULLS FIRST`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, oanl), sql: ` ORDER BY "a" ASC NULLS LAST`},
|
||||
commonSQLTestCase{gen: orderGen(csg, oanl), sql: ` ORDER BY "a" ASC NULLS LAST`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, od), sql: ` ORDER BY "a" DESC`},
|
||||
commonSQLTestCase{gen: orderGen(csg, od), sql: ` ORDER BY "a" DESC`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, odnf), sql: ` ORDER BY "a" DESC NULLS FIRST`},
|
||||
commonSQLTestCase{gen: orderGen(csg, odnf), sql: ` ORDER BY "a" DESC NULLS FIRST`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, odnl), sql: ` ORDER BY "a" DESC NULLS LAST`},
|
||||
commonSQLTestCase{gen: orderGen(csg, odnl), sql: ` ORDER BY "a" DESC NULLS LAST`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csg, oa, od), sql: ` ORDER BY "a" ASC, "a" DESC`},
|
||||
commonSQLTestCase{gen: orderGen(csg, oa, od), sql: ` ORDER BY "a" ASC, "a" DESC`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom), sql: ``},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oa), sql: ` order by "a" asc`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oa), sql: ` order by "a" asc`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oanf), sql: ` order by "a" asc nulls first`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oanf), sql: ` order by "a" asc nulls first`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oanl), sql: ` order by "a" asc nulls last`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oanl), sql: ` order by "a" asc nulls last`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, od), sql: ` order by "a" desc`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, od), sql: ` order by "a" desc`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, odnf), sql: ` order by "a" desc nulls first`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, odnf), sql: ` order by "a" desc nulls first`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, odnl), sql: ` order by "a" desc nulls last`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, odnl), sql: ` order by "a" desc nulls last`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oa, od), sql: ` order by "a" asc, "a" desc`},
|
||||
commonSQLTestCase{gen: orderGen(csgCustom, oa, od), sql: ` order by "a" asc, "a" desc`, isPrepared: true, args: emptyArgs},
|
||||
)
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestLimitSQL() {
|
||||
limitGen := func(csgs sqlgen.CommonSQLGenerator, l interface{}) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.LimitSQL(sb, l)
|
||||
}
|
||||
}
|
||||
|
||||
csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions())
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.LimitFragment = []byte(" limit ")
|
||||
csgCustom := sqlgen.NewCommonSQLGenerator("test", opts)
|
||||
|
||||
l := int64(10)
|
||||
la := exp.NewLiteralExpression("ALL")
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: limitGen(csg, nil), sql: ``},
|
||||
commonSQLTestCase{gen: limitGen(csg, nil), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: limitGen(csg, l), sql: ` LIMIT 10`},
|
||||
commonSQLTestCase{gen: limitGen(csg, l), sql: ` LIMIT ?`, isPrepared: true, args: []interface{}{l}},
|
||||
|
||||
commonSQLTestCase{gen: limitGen(csg, la), sql: ` LIMIT ALL`},
|
||||
commonSQLTestCase{gen: limitGen(csg, la), sql: ` LIMIT ALL`, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, nil), sql: ``},
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, nil), sql: ``, isPrepared: true, args: emptyArgs},
|
||||
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, l), sql: ` limit 10`},
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, l), sql: ` limit ?`, isPrepared: true, args: []interface{}{l}},
|
||||
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, la), sql: ` limit ALL`},
|
||||
commonSQLTestCase{gen: limitGen(csgCustom, la), sql: ` limit ALL`, isPrepared: true, args: emptyArgs},
|
||||
)
|
||||
}
|
||||
|
||||
func (csgs *commonSQLGeneratorSuite) TestUpdateExpressionSQL() {
|
||||
updateGen := func(csgs sqlgen.CommonSQLGenerator, ues ...exp.UpdateExpression) func(sb.SQLBuilder) {
|
||||
return func(sb sb.SQLBuilder) {
|
||||
csgs.UpdateExpressionSQL(sb, ues...)
|
||||
}
|
||||
}
|
||||
|
||||
csg := sqlgen.NewCommonSQLGenerator("test", sqlgen.DefaultDialectOptions())
|
||||
ue := exp.NewIdentifierExpression("", "", "col").Set("a")
|
||||
ue2 := exp.NewIdentifierExpression("", "", "col2").Set("b")
|
||||
|
||||
csgs.assertCases(
|
||||
commonSQLTestCase{gen: updateGen(csg), err: sqlgen.ErrNoUpdatedValuesProvided.Error()},
|
||||
commonSQLTestCase{gen: updateGen(csg), err: sqlgen.ErrNoUpdatedValuesProvided.Error()},
|
||||
|
||||
commonSQLTestCase{gen: updateGen(csg, ue), sql: `"col"='a'`},
|
||||
commonSQLTestCase{gen: updateGen(csg, ue), sql: `"col"=?`, isPrepared: true, args: []interface{}{"a"}},
|
||||
|
||||
commonSQLTestCase{gen: updateGen(csg, ue, ue2), sql: `"col"='a',"col2"='b'`},
|
||||
commonSQLTestCase{gen: updateGen(csg, ue, ue2), sql: `"col"=?,"col2"=?`, isPrepared: true, args: []interface{}{"a", "b"}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCommonSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(commonSQLGeneratorSuite))
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
DeleteSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, clauses exp.DeleteClauses)
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
deleteSQLGenerator struct {
|
||||
CommonSQLGenerator
|
||||
}
|
||||
)
|
||||
|
||||
var ErrNoSourceForDelete = errors.New("no source found when generating delete sql")
|
||||
|
||||
func NewDeleteSQLGenerator(dialect string, do *SQLDialectOptions) DeleteSQLGenerator {
|
||||
return &deleteSQLGenerator{NewCommonSQLGenerator(dialect, do)}
|
||||
}
|
||||
|
||||
func (dsg *deleteSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.DeleteClauses) {
|
||||
if !clauses.HasFrom() {
|
||||
b.SetError(ErrNoSourceForDelete)
|
||||
return
|
||||
}
|
||||
for _, f := range dsg.DialectOptions().DeleteSQLOrder {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
switch f {
|
||||
case CommonTableSQLFragment:
|
||||
dsg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables())
|
||||
case DeleteBeginSQLFragment:
|
||||
dsg.DeleteBeginSQL(
|
||||
b, exp.NewColumnListExpression(clauses.From()), !(clauses.HasLimit() || clauses.HasOrder()),
|
||||
)
|
||||
case FromSQLFragment:
|
||||
dsg.FromSQL(b, exp.NewColumnListExpression(clauses.From()))
|
||||
case WhereSQLFragment:
|
||||
dsg.WhereSQL(b, clauses.Where())
|
||||
case OrderSQLFragment:
|
||||
if dsg.DialectOptions().SupportsOrderByOnDelete {
|
||||
dsg.OrderSQL(b, clauses.Order())
|
||||
}
|
||||
case LimitSQLFragment:
|
||||
if dsg.DialectOptions().SupportsLimitOnDelete {
|
||||
dsg.LimitSQL(b, clauses.Limit())
|
||||
}
|
||||
case ReturningSQLFragment:
|
||||
dsg.ReturningSQL(b, clauses.Returning())
|
||||
default:
|
||||
b.SetError(ErrNotSupportedFragment("DELETE", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the correct fragment to being an DELETE statement
|
||||
func (dsg *deleteSQLGenerator) DeleteBeginSQL(b sb.SQLBuilder, from exp.ColumnListExpression, multiTable bool) {
|
||||
b.Write(dsg.DialectOptions().DeleteClause)
|
||||
if multiTable && dsg.DialectOptions().SupportsDeleteTableHint {
|
||||
dsg.SourcesSQL(b, from)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
deleteTestCase struct {
|
||||
clause exp.DeleteClauses
|
||||
sql string
|
||||
isPrepared bool
|
||||
args []interface{}
|
||||
err string
|
||||
}
|
||||
deleteSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) assertCases(dsg sqlgen.DeleteSQLGenerator, testCases ...deleteTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
dsg.Generate(b, tc.clause)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
dsgs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
dsgs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
dsgs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestDialect() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewDeleteSQLGenerator("test", opts)
|
||||
dsgs.Equal("test", d.Dialect())
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
d2 := sqlgen.NewDeleteSQLGenerator("test2", opts2)
|
||||
dsgs.Equal("test2", d2.Dialect())
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate() {
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", ""))
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
opts2.DeleteClause = []byte("delete")
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts2),
|
||||
deleteTestCase{clause: dc, sql: `delete FROM "test"`},
|
||||
deleteTestCase{clause: dc, sql: `delete FROM "test"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withUnsupportedFragment() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.DeleteSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment}
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", ""))
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, err: `db: unsupported DELETE SQL fragment InsertBeingSQLFragment`},
|
||||
deleteTestCase{clause: dc, err: `db: unsupported DELETE SQL fragment InsertBeingSQLFragment`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_noFrom() {
|
||||
dc := exp.NewDeleteClauses()
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
deleteTestCase{clause: dc, err: sqlgen.ErrNoSourceForDelete.Error()},
|
||||
deleteTestCase{clause: dc, err: sqlgen.ErrNoSourceForDelete.Error(), isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withErroredBuilder() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewDeleteSQLGenerator("test", opts)
|
||||
|
||||
dc := exp.NewDeleteClauses().SetFrom(exp.NewIdentifierExpression("", "test", ""))
|
||||
b := sb.NewSQLBuilder(false).SetError(errors.New("expected error"))
|
||||
d.Generate(b, dc)
|
||||
dsgs.assertErrorSQL(b, "db: expected error")
|
||||
|
||||
b = sb.NewSQLBuilder(true).SetError(errors.New("expected error"))
|
||||
d.Generate(b, dc)
|
||||
dsgs.assertErrorSQL(b, "db: expected error")
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withCommonTables() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.WithFragment = []byte("with ")
|
||||
opts.RecursiveFragment = []byte("recursive ")
|
||||
|
||||
tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil)
|
||||
|
||||
dc := exp.NewDeleteClauses().SetFrom(exp.NewIdentifierExpression("", "test_cte", ""))
|
||||
dcCte1 := dc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse))
|
||||
dcCte2 := dc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse))
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`},
|
||||
deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true},
|
||||
|
||||
deleteTestCase{clause: dcCte2, sql: `with recursive test_cte AS (select * from foo) DELETE FROM "test_cte"`},
|
||||
deleteTestCase{clause: dcCte2, sql: `with recursive test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsWithCTE = false
|
||||
expectedErr := sqlgen.ErrCTENotSupported("test")
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dcCte1, err: expectedErr.Error()},
|
||||
deleteTestCase{clause: dcCte1, err: expectedErr.Error(), isPrepared: true},
|
||||
|
||||
deleteTestCase{clause: dcCte2, err: expectedErr.Error()},
|
||||
deleteTestCase{clause: dcCte2, err: expectedErr.Error(), isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsWithCTE = true
|
||||
opts.SupportsWithCTERecursive = false
|
||||
expectedErr = sqlgen.ErrRecursiveCTENotSupported("test")
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`},
|
||||
deleteTestCase{clause: dcCte1, sql: `with test_cte AS (select * from foo) DELETE FROM "test_cte"`, isPrepared: true},
|
||||
|
||||
deleteTestCase{clause: dcCte2, err: expectedErr.Error()},
|
||||
deleteTestCase{clause: dcCte2, err: expectedErr.Error(), isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withWhere() {
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", "")).
|
||||
WhereAppend(exp.NewLiteralExpression(`"a"=?`, 1))
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" WHERE "a"=1`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" WHERE "a"=?`, isPrepared: true, args: []interface{}{
|
||||
int64(1),
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withOrder() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsOrderByOnDelete = true
|
||||
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetOrder(exp.NewIdentifierExpression("", "", "c").Desc())
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" ORDER BY "c" DESC`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" ORDER BY "c" DESC`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsOrderByOnDelete = false
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withLimit() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsLimitOnDelete = true
|
||||
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetLimit(1)
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" LIMIT 1`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" LIMIT ?`, isPrepared: true, args: []interface{}{int64(1)}},
|
||||
)
|
||||
|
||||
opts.SupportsLimitOnDelete = false
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (dsgs *deleteSQLGeneratorSuite) TestGenerate_withReturning() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsReturn = true
|
||||
|
||||
dc := exp.NewDeleteClauses().
|
||||
SetFrom(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetReturning(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" RETURNING "a", "b"`},
|
||||
deleteTestCase{clause: dc, sql: `DELETE FROM "test" RETURNING "a", "b"`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsReturn = false
|
||||
expectedErr := `db: dialect does not support RETURNING clause [dialect=test]`
|
||||
dsgs.assertCases(
|
||||
sqlgen.NewDeleteSQLGenerator("test", opts),
|
||||
deleteTestCase{clause: dc, err: expectedErr},
|
||||
deleteTestCase{clause: dc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func TestDeleteSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(deleteSQLGeneratorSuite))
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/internal/util"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
ExpressionSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, val interface{})
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
expressionSQLGenerator struct {
|
||||
dialect string
|
||||
dialectOptions *SQLDialectOptions
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
replacementRune = '?'
|
||||
TrueLiteral = exp.NewLiteralExpression("TRUE")
|
||||
FalseLiteral = exp.NewLiteralExpression("FALSE")
|
||||
|
||||
ErrEmptyIdentifier = errors.New(
|
||||
`a empty identifier was encountered, please specify a "schema", "table" or "column"`,
|
||||
)
|
||||
ErrUnexpectedNamedWindow = errors.New(`unexpected named window function`)
|
||||
ErrEmptyCaseWhens = errors.New(`when conditions not found for case statement`)
|
||||
)
|
||||
|
||||
func errUnsupportedExpressionType(e exp.Expression) error {
|
||||
return errors.New("unsupported expression type %T", e)
|
||||
}
|
||||
|
||||
func errUnsupportedIdentifierExpression(t interface{}) error {
|
||||
return errors.New("unexpected col type must be string or LiteralExpression received %T", t)
|
||||
}
|
||||
|
||||
func errUnsupportedBooleanExpressionOperator(op exp.BooleanOperation) error {
|
||||
return errors.New("boolean operator '%+v' not supported", op)
|
||||
}
|
||||
|
||||
func errUnsupportedBitwiseExpressionOperator(op exp.BitwiseOperation) error {
|
||||
return errors.New("bitwise operator '%+v' not supported", op)
|
||||
}
|
||||
|
||||
func errUnsupportedRangeExpressionOperator(op exp.RangeOperation) error {
|
||||
return errors.New("range operator %+v not supported", op)
|
||||
}
|
||||
|
||||
func errLateralNotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support lateral expressions [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func NewExpressionSQLGenerator(dialect string, do *SQLDialectOptions) ExpressionSQLGenerator {
|
||||
return &expressionSQLGenerator{dialect: dialect, dialectOptions: do}
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) Dialect() string {
|
||||
return esg.dialect
|
||||
}
|
||||
|
||||
var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
||||
|
||||
func (esg *expressionSQLGenerator) Generate(b sb.SQLBuilder, val interface{}) {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
if val == nil {
|
||||
esg.literalNil(b)
|
||||
return
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case exp.Expression:
|
||||
esg.expressionSQL(b, v)
|
||||
case int:
|
||||
esg.literalInt(b, int64(v))
|
||||
case int32:
|
||||
esg.literalInt(b, int64(v))
|
||||
case int64:
|
||||
esg.literalInt(b, v)
|
||||
case float32:
|
||||
esg.literalFloat(b, float64(v))
|
||||
case float64:
|
||||
esg.literalFloat(b, v)
|
||||
case string:
|
||||
esg.literalString(b, v)
|
||||
case bool:
|
||||
esg.literalBool(b, v)
|
||||
case time.Time:
|
||||
esg.literalTime(b, v)
|
||||
case *time.Time:
|
||||
if v == nil {
|
||||
esg.literalNil(b)
|
||||
return
|
||||
}
|
||||
esg.literalTime(b, *v)
|
||||
case driver.Valuer:
|
||||
// See https://github.com/golang/go/commit/0ce1d79a6a771f7449ec493b993ed2a720917870
|
||||
if rv := reflect.ValueOf(val); rv.Kind() == reflect.Ptr &&
|
||||
rv.IsNil() &&
|
||||
rv.Type().Elem().Implements(valuerReflectType) {
|
||||
esg.literalNil(b)
|
||||
return
|
||||
}
|
||||
dVal, err := v.Value()
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
esg.Generate(b, dVal)
|
||||
default:
|
||||
esg.reflectSQL(b, val)
|
||||
}
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) reflectSQL(b sb.SQLBuilder, val interface{}) {
|
||||
v := reflect.Indirect(reflect.ValueOf(val))
|
||||
valKind := v.Kind()
|
||||
switch {
|
||||
case util.IsInvalid(valKind):
|
||||
esg.literalNil(b)
|
||||
case util.IsSlice(valKind):
|
||||
switch t := val.(type) {
|
||||
case []byte:
|
||||
esg.literalBytes(b, t)
|
||||
case []exp.CommonTableExpression:
|
||||
esg.commonTablesSliceSQL(b, t)
|
||||
default:
|
||||
esg.sliceValueSQL(b, v)
|
||||
}
|
||||
case util.IsInt(valKind):
|
||||
esg.Generate(b, v.Int())
|
||||
case util.IsUint(valKind):
|
||||
esg.Generate(b, int64(v.Uint()))
|
||||
case util.IsFloat(valKind):
|
||||
esg.Generate(b, v.Float())
|
||||
case util.IsString(valKind):
|
||||
esg.Generate(b, v.String())
|
||||
case util.IsBool(valKind):
|
||||
esg.Generate(b, v.Bool())
|
||||
default:
|
||||
b.SetError(errors.NewEncodeError(val))
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gocyclo // not complex just long
|
||||
func (esg *expressionSQLGenerator) expressionSQL(b sb.SQLBuilder, expression exp.Expression) {
|
||||
switch e := expression.(type) {
|
||||
case exp.ColumnListExpression:
|
||||
esg.columnListSQL(b, e)
|
||||
case exp.ExpressionList:
|
||||
esg.expressionListSQL(b, e)
|
||||
case exp.LiteralExpression:
|
||||
esg.literalExpressionSQL(b, e)
|
||||
case exp.IdentifierExpression:
|
||||
esg.identifierExpressionSQL(b, e)
|
||||
case exp.LateralExpression:
|
||||
esg.lateralExpressionSQL(b, e)
|
||||
case exp.AliasedExpression:
|
||||
esg.aliasedExpressionSQL(b, e)
|
||||
case exp.BooleanExpression:
|
||||
esg.booleanExpressionSQL(b, e)
|
||||
case exp.BitwiseExpression:
|
||||
esg.bitwiseExpressionSQL(b, e)
|
||||
case exp.RangeExpression:
|
||||
esg.rangeExpressionSQL(b, e)
|
||||
case exp.OrderedExpression:
|
||||
esg.orderedExpressionSQL(b, e)
|
||||
case exp.UpdateExpression:
|
||||
esg.updateExpressionSQL(b, e)
|
||||
case exp.SQLFunctionExpression:
|
||||
esg.sqlFunctionExpressionSQL(b, e)
|
||||
case exp.SQLWindowFunctionExpression:
|
||||
esg.sqlWindowFunctionExpression(b, e)
|
||||
case exp.WindowExpression:
|
||||
esg.windowExpressionSQL(b, e)
|
||||
case exp.CastExpression:
|
||||
esg.castExpressionSQL(b, e)
|
||||
case exp.AppendableExpression:
|
||||
esg.appendableExpressionSQL(b, e)
|
||||
case exp.CommonTableExpression:
|
||||
esg.commonTableExpressionSQL(b, e)
|
||||
case exp.CompoundExpression:
|
||||
esg.compoundExpressionSQL(b, e)
|
||||
case exp.CaseExpression:
|
||||
esg.caseExpressionSQL(b, e)
|
||||
case exp.Ex:
|
||||
esg.expressionMapSQL(b, e)
|
||||
case exp.ExOr:
|
||||
esg.expressionOrMapSQL(b, e)
|
||||
default:
|
||||
b.SetError(errUnsupportedExpressionType(e))
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a placeholder (e.g. ?, $1)
|
||||
func (esg *expressionSQLGenerator) placeHolderSQL(b sb.SQLBuilder, i interface{}) {
|
||||
b.Write(esg.dialectOptions.PlaceHolderFragment)
|
||||
if esg.dialectOptions.IncludePlaceholderNum {
|
||||
b.WriteStrings(strconv.FormatInt(int64(b.CurrentArgPosition()), 10))
|
||||
}
|
||||
b.WriteArg(i)
|
||||
}
|
||||
|
||||
// Generates creates the sql for a sub select on a Dataset
|
||||
func (esg *expressionSQLGenerator) appendableExpressionSQL(b sb.SQLBuilder, a exp.AppendableExpression) {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
a.AppendSQL(b)
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
if a.GetAs() != nil {
|
||||
b.Write(esg.dialectOptions.AsFragment)
|
||||
esg.Generate(b, a.GetAs())
|
||||
}
|
||||
}
|
||||
|
||||
// Quotes an identifier (e.g. "col", "table"."col"
|
||||
func (esg *expressionSQLGenerator) identifierExpressionSQL(b sb.SQLBuilder, ident exp.IdentifierExpression) {
|
||||
if ident.IsEmpty() {
|
||||
b.SetError(ErrEmptyIdentifier)
|
||||
return
|
||||
}
|
||||
schema, table, col := ident.GetSchema(), ident.GetTable(), ident.GetCol()
|
||||
if schema != esg.dialectOptions.EmptyString {
|
||||
b.WriteRunes(esg.dialectOptions.QuoteRune).
|
||||
WriteStrings(schema).
|
||||
WriteRunes(esg.dialectOptions.QuoteRune)
|
||||
}
|
||||
if table != esg.dialectOptions.EmptyString {
|
||||
if schema != esg.dialectOptions.EmptyString {
|
||||
b.WriteRunes(esg.dialectOptions.PeriodRune)
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.QuoteRune).
|
||||
WriteStrings(table).
|
||||
WriteRunes(esg.dialectOptions.QuoteRune)
|
||||
}
|
||||
switch t := col.(type) {
|
||||
case nil:
|
||||
case string:
|
||||
if col != esg.dialectOptions.EmptyString {
|
||||
if table != esg.dialectOptions.EmptyString || schema != esg.dialectOptions.EmptyString {
|
||||
b.WriteRunes(esg.dialectOptions.PeriodRune)
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.QuoteRune).
|
||||
WriteStrings(t).
|
||||
WriteRunes(esg.dialectOptions.QuoteRune)
|
||||
}
|
||||
case exp.LiteralExpression:
|
||||
if table != esg.dialectOptions.EmptyString || schema != esg.dialectOptions.EmptyString {
|
||||
b.WriteRunes(esg.dialectOptions.PeriodRune)
|
||||
}
|
||||
esg.Generate(b, t)
|
||||
default:
|
||||
b.SetError(errUnsupportedIdentifierExpression(col))
|
||||
}
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) lateralExpressionSQL(b sb.SQLBuilder, le exp.LateralExpression) {
|
||||
if !esg.dialectOptions.SupportsLateral {
|
||||
b.SetError(errLateralNotSupported(esg.dialect))
|
||||
return
|
||||
}
|
||||
b.Write(esg.dialectOptions.LateralFragment)
|
||||
esg.Generate(b, le.Table())
|
||||
}
|
||||
|
||||
// Generates SQL NULL value
|
||||
func (esg *expressionSQLGenerator) literalNil(b sb.SQLBuilder) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, nil)
|
||||
return
|
||||
}
|
||||
b.Write(esg.dialectOptions.Null)
|
||||
}
|
||||
|
||||
// Generates SQL bool literal, (e.g. TRUE, FALSE, mysql 1, 0, sqlite3 1, 0)
|
||||
func (esg *expressionSQLGenerator) literalBool(b sb.SQLBuilder, bl bool) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, bl)
|
||||
return
|
||||
}
|
||||
if bl {
|
||||
b.Write(esg.dialectOptions.True)
|
||||
} else {
|
||||
b.Write(esg.dialectOptions.False)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates SQL for a time.Time value
|
||||
func (esg *expressionSQLGenerator) literalTime(b sb.SQLBuilder, t time.Time) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, t)
|
||||
return
|
||||
}
|
||||
esg.Generate(b, t.In(timeLocation).Format(esg.dialectOptions.TimeFormat))
|
||||
}
|
||||
|
||||
// Generates SQL for a Float Value
|
||||
func (esg *expressionSQLGenerator) literalFloat(b sb.SQLBuilder, f float64) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, f)
|
||||
return
|
||||
}
|
||||
b.WriteStrings(strconv.FormatFloat(f, 'f', -1, 64))
|
||||
}
|
||||
|
||||
// Generates SQL for an int value
|
||||
func (esg *expressionSQLGenerator) literalInt(b sb.SQLBuilder, i int64) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, i)
|
||||
return
|
||||
}
|
||||
b.WriteStrings(strconv.FormatInt(i, 10))
|
||||
}
|
||||
|
||||
// Generates SQL for a string
|
||||
func (esg *expressionSQLGenerator) literalString(b sb.SQLBuilder, s string) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, s)
|
||||
return
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.StringQuote)
|
||||
for _, char := range s {
|
||||
if e, ok := esg.dialectOptions.EscapedRunes[char]; ok {
|
||||
b.Write(e)
|
||||
} else {
|
||||
b.WriteRunes(char)
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteRunes(esg.dialectOptions.StringQuote)
|
||||
}
|
||||
|
||||
// Generates SQL for a slice of bytes
|
||||
func (esg *expressionSQLGenerator) literalBytes(b sb.SQLBuilder, bs []byte) {
|
||||
if b.IsPrepared() {
|
||||
esg.placeHolderSQL(b, bs)
|
||||
return
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.StringQuote)
|
||||
i := 0
|
||||
for len(bs) > 0 {
|
||||
char, l := utf8.DecodeRune(bs)
|
||||
if e, ok := esg.dialectOptions.EscapedRunes[char]; ok {
|
||||
b.Write(e)
|
||||
} else {
|
||||
b.WriteRunes(char)
|
||||
}
|
||||
i++
|
||||
bs = bs[l:]
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.StringQuote)
|
||||
}
|
||||
|
||||
// Generates SQL for a slice of values (e.g. []int64{1,2,3,4} -> (1,2,3,4)
|
||||
func (esg *expressionSQLGenerator) sliceValueSQL(b sb.SQLBuilder, slice reflect.Value) {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
for i, l := 0, slice.Len(); i < l; i++ {
|
||||
esg.Generate(b, slice.Index(i).Interface())
|
||||
if i < l-1 {
|
||||
b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for an AliasedExpression (e.g. I("a").As("b") -> "a" AS "b")
|
||||
func (esg *expressionSQLGenerator) aliasedExpressionSQL(b sb.SQLBuilder, aliased exp.AliasedExpression) {
|
||||
esg.Generate(b, aliased.Aliased())
|
||||
b.Write(esg.dialectOptions.AsFragment)
|
||||
esg.Generate(b, aliased.GetAs())
|
||||
}
|
||||
|
||||
// Generates SQL for a BooleanExpresion (e.g. I("a").Eq(2) -> "a" = 2)
|
||||
func (esg *expressionSQLGenerator) booleanExpressionSQL(b sb.SQLBuilder, operator exp.BooleanExpression) {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
esg.Generate(b, operator.LHS())
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
operatorOp := operator.Op()
|
||||
if val, ok := esg.dialectOptions.BooleanOperatorLookup[operatorOp]; ok {
|
||||
b.Write(val)
|
||||
} else {
|
||||
b.SetError(errUnsupportedBooleanExpressionOperator(operatorOp))
|
||||
return
|
||||
}
|
||||
rhs := operator.RHS()
|
||||
|
||||
if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && rhs != nil && !esg.dialectOptions.BooleanDataTypeSupported {
|
||||
b.SetError(errors.New("boolean data type is not supported by dialect %q", esg.dialect))
|
||||
return
|
||||
}
|
||||
|
||||
if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && esg.dialectOptions.UseLiteralIsBools {
|
||||
// these values must be interpolated because preparing them generates invalid SQL
|
||||
switch rhs {
|
||||
case true:
|
||||
rhs = TrueLiteral
|
||||
case false:
|
||||
rhs = FalseLiteral
|
||||
case nil:
|
||||
rhs = exp.NewLiteralExpression(string(esg.dialectOptions.Null))
|
||||
}
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
|
||||
if (operatorOp == exp.IsOp || operatorOp == exp.IsNotOp) && rhs == nil && !esg.dialectOptions.BooleanDataTypeSupported {
|
||||
// e.g. for SQL server dialect which does not support "IS @p1" for "IS NULL"
|
||||
b.Write(esg.dialectOptions.Null)
|
||||
} else {
|
||||
esg.Generate(b, rhs)
|
||||
}
|
||||
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for a BitwiseExpresion (e.g. I("a").BitwiseOr(2) - > "a" | 2)
|
||||
func (esg *expressionSQLGenerator) bitwiseExpressionSQL(b sb.SQLBuilder, operator exp.BitwiseExpression) {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
|
||||
if operator.LHS() != nil {
|
||||
esg.Generate(b, operator.LHS())
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
|
||||
operatorOp := operator.Op()
|
||||
if val, ok := esg.dialectOptions.BitwiseOperatorLookup[operatorOp]; ok {
|
||||
b.Write(val)
|
||||
} else {
|
||||
b.SetError(errUnsupportedBitwiseExpressionOperator(operatorOp))
|
||||
return
|
||||
}
|
||||
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
esg.Generate(b, operator.RHS())
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for a RangeExpresion (e.g. I("a").Between(RangeVal{Start:2,End:5}) -> "a" BETWEEN 2 AND 5)
|
||||
func (esg *expressionSQLGenerator) rangeExpressionSQL(b sb.SQLBuilder, operator exp.RangeExpression) {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
esg.Generate(b, operator.LHS())
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
operatorOp := operator.Op()
|
||||
if val, ok := esg.dialectOptions.RangeOperatorLookup[operatorOp]; ok {
|
||||
b.Write(val)
|
||||
} else {
|
||||
b.SetError(errUnsupportedRangeExpressionOperator(operatorOp))
|
||||
return
|
||||
}
|
||||
rhs := operator.RHS()
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
esg.Generate(b, rhs.Start())
|
||||
b.Write(esg.dialectOptions.AndFragment)
|
||||
esg.Generate(b, rhs.End())
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for an OrderedExpression (e.g. I("a").Asc() -> "a" ASC)
|
||||
func (esg *expressionSQLGenerator) orderedExpressionSQL(b sb.SQLBuilder, order exp.OrderedExpression) {
|
||||
esg.Generate(b, order.SortExpression())
|
||||
if order.IsAsc() {
|
||||
b.Write(esg.dialectOptions.AscFragment)
|
||||
} else {
|
||||
b.Write(esg.dialectOptions.DescFragment)
|
||||
}
|
||||
switch order.NullSortType() {
|
||||
case exp.NoNullsSortType:
|
||||
return
|
||||
case exp.NullsFirstSortType:
|
||||
b.Write(esg.dialectOptions.NullsFirstFragment)
|
||||
case exp.NullsLastSortType:
|
||||
b.Write(esg.dialectOptions.NullsLastFragment)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates SQL for an ExpressionList (e.g. And(I("a").Eq("a"), I("b").Eq("b")) -> (("a" = 'a') AND ("b" = 'b')))
|
||||
func (esg *expressionSQLGenerator) expressionListSQL(b sb.SQLBuilder, expressionList exp.ExpressionList) {
|
||||
if expressionList.IsEmpty() {
|
||||
return
|
||||
}
|
||||
var op []byte
|
||||
if expressionList.Type() == exp.AndType {
|
||||
op = esg.dialectOptions.AndFragment
|
||||
} else {
|
||||
op = esg.dialectOptions.OrFragment
|
||||
}
|
||||
exps := expressionList.Expressions()
|
||||
expLen := len(exps) - 1
|
||||
if expLen > 0 {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
} else {
|
||||
esg.Generate(b, exps[0])
|
||||
return
|
||||
}
|
||||
for i, e := range exps {
|
||||
esg.Generate(b, e)
|
||||
if i < expLen {
|
||||
b.Write(op)
|
||||
}
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for a ColumnListExpression
|
||||
func (esg *expressionSQLGenerator) columnListSQL(b sb.SQLBuilder, columnList exp.ColumnListExpression) {
|
||||
cols := columnList.Columns()
|
||||
colLen := len(cols)
|
||||
for i, col := range cols {
|
||||
esg.Generate(b, col)
|
||||
if i < colLen-1 {
|
||||
b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates SQL for an UpdateEpxresion
|
||||
func (esg *expressionSQLGenerator) updateExpressionSQL(b sb.SQLBuilder, update exp.UpdateExpression) {
|
||||
esg.Generate(b, update.Col())
|
||||
b.WriteRunes(esg.dialectOptions.SetOperatorRune)
|
||||
esg.Generate(b, update.Val())
|
||||
}
|
||||
|
||||
// Generates SQL for a LiteralExpression
|
||||
//
|
||||
// L("a + b") -> a + b
|
||||
// L("a = ?", 1) -> a = 1
|
||||
func (esg *expressionSQLGenerator) literalExpressionSQL(b sb.SQLBuilder, literal exp.LiteralExpression) {
|
||||
l := literal.Literal()
|
||||
args := literal.Args()
|
||||
if argsLen := len(args); argsLen > 0 {
|
||||
currIndex := 0
|
||||
for _, char := range l {
|
||||
if char == replacementRune && currIndex < argsLen {
|
||||
esg.Generate(b, args[currIndex])
|
||||
currIndex++
|
||||
} else {
|
||||
b.WriteRunes(char)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
b.WriteStrings(l)
|
||||
}
|
||||
|
||||
// Generates SQL for a SQLFunctionExpression
|
||||
//
|
||||
// COUNT(I("a")) -> COUNT("a")
|
||||
func (esg *expressionSQLGenerator) sqlFunctionExpressionSQL(b sb.SQLBuilder, sqlFunc exp.SQLFunctionExpression) {
|
||||
b.WriteStrings(sqlFunc.Name())
|
||||
esg.Generate(b, sqlFunc.Args())
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) sqlWindowFunctionExpression(b sb.SQLBuilder, sqlWinFunc exp.SQLWindowFunctionExpression) {
|
||||
if !esg.dialectOptions.SupportsWindowFunction {
|
||||
b.SetError(ErrWindowNotSupported(esg.dialect))
|
||||
return
|
||||
}
|
||||
esg.Generate(b, sqlWinFunc.Func())
|
||||
b.Write(esg.dialectOptions.WindowOverFragment)
|
||||
switch {
|
||||
case sqlWinFunc.HasWindowName():
|
||||
esg.Generate(b, sqlWinFunc.WindowName())
|
||||
case sqlWinFunc.HasWindow():
|
||||
if sqlWinFunc.Window().HasName() {
|
||||
b.SetError(ErrUnexpectedNamedWindow)
|
||||
return
|
||||
}
|
||||
esg.Generate(b, sqlWinFunc.Window())
|
||||
default:
|
||||
esg.Generate(b, exp.NewWindowExpression(nil, nil, nil, nil))
|
||||
}
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) windowExpressionSQL(b sb.SQLBuilder, we exp.WindowExpression) {
|
||||
if !esg.dialectOptions.SupportsWindowFunction {
|
||||
b.SetError(ErrWindowNotSupported(esg.dialect))
|
||||
return
|
||||
}
|
||||
if we.HasName() {
|
||||
esg.Generate(b, we.Name())
|
||||
b.Write(esg.dialectOptions.AsFragment)
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
|
||||
hasPartition := we.HasPartitionBy()
|
||||
hasOrder := we.HasOrder()
|
||||
|
||||
if we.HasParent() {
|
||||
esg.Generate(b, we.Parent())
|
||||
if hasPartition || hasOrder {
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
}
|
||||
|
||||
if hasPartition {
|
||||
b.Write(esg.dialectOptions.WindowPartitionByFragment)
|
||||
esg.Generate(b, we.PartitionCols())
|
||||
if hasOrder {
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
}
|
||||
if hasOrder {
|
||||
b.Write(esg.dialectOptions.WindowOrderByFragment)
|
||||
esg.Generate(b, we.OrderCols())
|
||||
}
|
||||
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates SQL for a CastExpression
|
||||
//
|
||||
// I("a").Cast("NUMERIC") -> CAST("a" AS NUMERIC)
|
||||
func (esg *expressionSQLGenerator) castExpressionSQL(b sb.SQLBuilder, cast exp.CastExpression) {
|
||||
b.Write(esg.dialectOptions.CastFragment).WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
esg.Generate(b, cast.Casted())
|
||||
b.Write(esg.dialectOptions.AsFragment)
|
||||
esg.Generate(b, cast.Type())
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
}
|
||||
|
||||
// Generates the sql for the WITH clauses for common table expressions (CTE)
|
||||
func (esg *expressionSQLGenerator) commonTablesSliceSQL(b sb.SQLBuilder, ctes []exp.CommonTableExpression) {
|
||||
l := len(ctes)
|
||||
if l == 0 {
|
||||
return
|
||||
}
|
||||
if !esg.dialectOptions.SupportsWithCTE {
|
||||
b.SetError(ErrCTENotSupported(esg.dialect))
|
||||
return
|
||||
}
|
||||
b.Write(esg.dialectOptions.WithFragment)
|
||||
anyRecursive := false
|
||||
for _, cte := range ctes {
|
||||
anyRecursive = anyRecursive || cte.IsRecursive()
|
||||
}
|
||||
if anyRecursive {
|
||||
if !esg.dialectOptions.SupportsWithCTERecursive {
|
||||
b.SetError(ErrRecursiveCTENotSupported(esg.dialect))
|
||||
return
|
||||
}
|
||||
b.Write(esg.dialectOptions.RecursiveFragment)
|
||||
}
|
||||
for i, cte := range ctes {
|
||||
esg.Generate(b, cte)
|
||||
if i < l-1 {
|
||||
b.WriteRunes(esg.dialectOptions.CommaRune, esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
}
|
||||
b.WriteRunes(esg.dialectOptions.SpaceRune)
|
||||
}
|
||||
|
||||
// Generates SQL for a CommonTableExpression
|
||||
func (esg *expressionSQLGenerator) commonTableExpressionSQL(b sb.SQLBuilder, cte exp.CommonTableExpression) {
|
||||
esg.Generate(b, cte.Name())
|
||||
b.Write(esg.dialectOptions.AsFragment)
|
||||
esg.Generate(b, cte.SubQuery())
|
||||
}
|
||||
|
||||
// Generates SQL for a CompoundExpression
|
||||
func (esg *expressionSQLGenerator) compoundExpressionSQL(b sb.SQLBuilder, compound exp.CompoundExpression) {
|
||||
switch compound.Type() {
|
||||
case exp.UnionCompoundType:
|
||||
b.Write(esg.dialectOptions.UnionFragment)
|
||||
case exp.UnionAllCompoundType:
|
||||
b.Write(esg.dialectOptions.UnionAllFragment)
|
||||
case exp.IntersectCompoundType:
|
||||
b.Write(esg.dialectOptions.IntersectFragment)
|
||||
case exp.IntersectAllCompoundType:
|
||||
b.Write(esg.dialectOptions.IntersectAllFragment)
|
||||
}
|
||||
if esg.dialectOptions.WrapCompoundsInParens {
|
||||
b.WriteRunes(esg.dialectOptions.LeftParenRune)
|
||||
compound.RHS().AppendSQL(b)
|
||||
b.WriteRunes(esg.dialectOptions.RightParenRune)
|
||||
} else {
|
||||
compound.RHS().AppendSQL(b)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates SQL for a CaseExpression
|
||||
func (esg *expressionSQLGenerator) caseExpressionSQL(b sb.SQLBuilder, caseExpression exp.CaseExpression) {
|
||||
caseVal := caseExpression.GetValue()
|
||||
whens := caseExpression.GetWhens()
|
||||
elseResult := caseExpression.GetElse()
|
||||
|
||||
if len(whens) == 0 {
|
||||
b.SetError(ErrEmptyCaseWhens)
|
||||
return
|
||||
}
|
||||
b.Write(esg.dialectOptions.CaseFragment)
|
||||
if caseVal != nil {
|
||||
esg.Generate(b, caseVal)
|
||||
}
|
||||
for _, when := range whens {
|
||||
b.Write(esg.dialectOptions.WhenFragment)
|
||||
esg.Generate(b, when.Condition())
|
||||
b.Write(esg.dialectOptions.ThenFragment)
|
||||
esg.Generate(b, when.Result())
|
||||
}
|
||||
if elseResult != nil {
|
||||
b.Write(esg.dialectOptions.ElseFragment)
|
||||
esg.Generate(b, elseResult.Result())
|
||||
}
|
||||
b.Write(esg.dialectOptions.EndFragment)
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) expressionMapSQL(b sb.SQLBuilder, ex exp.Ex) {
|
||||
expressionList, err := ex.ToExpressions()
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
esg.Generate(b, expressionList)
|
||||
}
|
||||
|
||||
func (esg *expressionSQLGenerator) expressionOrMapSQL(b sb.SQLBuilder, ex exp.ExOr) {
|
||||
expressionList, err := ex.ToExpressions()
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
esg.Generate(b, expressionList)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,203 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
InsertSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, clauses exp.InsertClauses)
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
insertSQLGenerator struct {
|
||||
CommonSQLGenerator
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConflictUpdateValuesRequired = errors.New("values are required for on conflict update expression")
|
||||
ErrNoSourceForInsert = errors.New("no source found when generating insert sql")
|
||||
)
|
||||
|
||||
func errMisMatchedRowLength(expectedL, actualL int) error {
|
||||
return errors.New("rows with different value length expected %d got %d", expectedL, actualL)
|
||||
}
|
||||
|
||||
func errUpsertWithWhereNotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support upsert with where clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func NewInsertSQLGenerator(dialect string, do *SQLDialectOptions) InsertSQLGenerator {
|
||||
return &insertSQLGenerator{NewCommonSQLGenerator(dialect, do)}
|
||||
}
|
||||
|
||||
func (isg *insertSQLGenerator) Generate(
|
||||
b sb.SQLBuilder,
|
||||
clauses exp.InsertClauses,
|
||||
) {
|
||||
if !clauses.HasInto() {
|
||||
b.SetError(ErrNoSourceForInsert)
|
||||
return
|
||||
}
|
||||
for _, f := range isg.DialectOptions().InsertSQLOrder {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
switch f {
|
||||
case CommonTableSQLFragment:
|
||||
isg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables())
|
||||
case InsertBeingSQLFragment:
|
||||
isg.InsertBeginSQL(b, clauses.OnConflict())
|
||||
case IntoSQLFragment:
|
||||
b.WriteRunes(isg.DialectOptions().SpaceRune)
|
||||
isg.ExpressionSQLGenerator().Generate(b, clauses.Into())
|
||||
case InsertSQLFragment:
|
||||
isg.InsertSQL(b, clauses)
|
||||
case ReturningSQLFragment:
|
||||
isg.ReturningSQL(b, clauses.Returning())
|
||||
default:
|
||||
b.SetError(ErrNotSupportedFragment("INSERT", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the correct fragment to being an INSERT statement
|
||||
func (isg *insertSQLGenerator) InsertBeginSQL(b sb.SQLBuilder, o exp.ConflictExpression) {
|
||||
if isg.DialectOptions().SupportsInsertIgnoreSyntax && o != nil {
|
||||
b.Write(isg.DialectOptions().InsertIgnoreClause)
|
||||
} else {
|
||||
b.Write(isg.DialectOptions().InsertClause)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the columns list to an insert statement
|
||||
func (isg *insertSQLGenerator) InsertSQL(b sb.SQLBuilder, ic exp.InsertClauses) {
|
||||
switch {
|
||||
case ic.HasRows():
|
||||
ie, err := exp.NewInsertExpression(ic.Rows()...)
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
isg.InsertExpressionSQL(b, ie)
|
||||
case ic.HasCols() && ic.HasVals():
|
||||
isg.insertColumnsSQL(b, ic.Cols())
|
||||
isg.insertValuesSQL(b, ic.Vals())
|
||||
case ic.HasCols() && ic.HasFrom():
|
||||
isg.insertColumnsSQL(b, ic.Cols())
|
||||
isg.insertFromSQL(b, ic.From())
|
||||
case ic.HasFrom():
|
||||
isg.insertFromSQL(b, ic.From())
|
||||
default:
|
||||
isg.defaultValuesSQL(b)
|
||||
}
|
||||
if ic.HasAlias() {
|
||||
b.Write(isg.DialectOptions().AsFragment)
|
||||
isg.ExpressionSQLGenerator().Generate(b, ic.Alias())
|
||||
}
|
||||
isg.onConflictSQL(b, ic.OnConflict())
|
||||
}
|
||||
|
||||
func (isg *insertSQLGenerator) InsertExpressionSQL(b sb.SQLBuilder, ie exp.InsertExpression) {
|
||||
switch {
|
||||
case ie.IsInsertFrom():
|
||||
isg.insertFromSQL(b, ie.From())
|
||||
case ie.IsEmpty():
|
||||
isg.defaultValuesSQL(b)
|
||||
default:
|
||||
isg.insertColumnsSQL(b, ie.Cols())
|
||||
isg.insertValuesSQL(b, ie.Vals())
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the DefaultValuesFragment to an SQL statement
|
||||
func (isg *insertSQLGenerator) defaultValuesSQL(b sb.SQLBuilder) {
|
||||
b.Write(isg.DialectOptions().DefaultValuesFragment)
|
||||
}
|
||||
|
||||
func (isg *insertSQLGenerator) insertFromSQL(b sb.SQLBuilder, ae exp.AppendableExpression) {
|
||||
b.WriteRunes(isg.DialectOptions().SpaceRune)
|
||||
ae.AppendSQL(b)
|
||||
}
|
||||
|
||||
// Adds the columns list to an insert statement
|
||||
func (isg *insertSQLGenerator) insertColumnsSQL(b sb.SQLBuilder, cols exp.ColumnListExpression) {
|
||||
b.WriteRunes(isg.DialectOptions().SpaceRune, isg.DialectOptions().LeftParenRune)
|
||||
isg.ExpressionSQLGenerator().Generate(b, cols)
|
||||
b.WriteRunes(isg.DialectOptions().RightParenRune)
|
||||
}
|
||||
|
||||
// Adds the values clause to an SQL statement
|
||||
func (isg *insertSQLGenerator) insertValuesSQL(b sb.SQLBuilder, values [][]interface{}) {
|
||||
b.Write(isg.DialectOptions().ValuesFragment)
|
||||
rowLen := len(values[0])
|
||||
valueLen := len(values)
|
||||
for i, row := range values {
|
||||
if len(row) != rowLen {
|
||||
b.SetError(errMisMatchedRowLength(rowLen, len(row)))
|
||||
return
|
||||
}
|
||||
isg.ExpressionSQLGenerator().Generate(b, row)
|
||||
if i < valueLen-1 {
|
||||
b.WriteRunes(isg.DialectOptions().CommaRune, isg.DialectOptions().SpaceRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the DefaultValuesFragment to an SQL statement
|
||||
func (isg *insertSQLGenerator) onConflictSQL(b sb.SQLBuilder, o exp.ConflictExpression) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
b.Write(isg.DialectOptions().ConflictFragment)
|
||||
switch t := o.(type) {
|
||||
case exp.ConflictUpdateExpression:
|
||||
target := t.TargetColumn()
|
||||
if isg.DialectOptions().SupportsConflictTarget && target != "" {
|
||||
wrapParens := !strings.HasPrefix(strings.ToLower(target), "on constraint")
|
||||
|
||||
b.WriteRunes(isg.DialectOptions().SpaceRune)
|
||||
if wrapParens {
|
||||
b.WriteRunes(isg.DialectOptions().LeftParenRune).
|
||||
WriteStrings(target).
|
||||
WriteRunes(isg.DialectOptions().RightParenRune)
|
||||
} else {
|
||||
b.Write([]byte(target))
|
||||
}
|
||||
}
|
||||
isg.onConflictDoUpdateSQL(b, t)
|
||||
default:
|
||||
b.Write(isg.DialectOptions().ConflictDoNothingFragment)
|
||||
}
|
||||
}
|
||||
|
||||
func (isg *insertSQLGenerator) onConflictDoUpdateSQL(b sb.SQLBuilder, o exp.ConflictUpdateExpression) {
|
||||
b.Write(isg.DialectOptions().ConflictDoUpdateFragment)
|
||||
update := o.Update()
|
||||
if update == nil {
|
||||
b.SetError(ErrConflictUpdateValuesRequired)
|
||||
return
|
||||
}
|
||||
ue, err := exp.NewUpdateExpressions(update)
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
isg.UpdateExpressionSQL(b, ue...)
|
||||
if b.Error() == nil && o.WhereClause() != nil {
|
||||
if !isg.DialectOptions().SupportsConflictUpdateWhere {
|
||||
b.SetError(errUpsertWithWhereNotSupported(isg.Dialect()))
|
||||
return
|
||||
}
|
||||
isg.WhereSQL(b, o.WhereClause())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
insertTestCase struct {
|
||||
clause exp.InsertClauses
|
||||
sql string
|
||||
isPrepared bool
|
||||
args []interface{}
|
||||
err string
|
||||
}
|
||||
insertSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) assertCases(isg sqlgen.InsertSQLGenerator, testCases ...insertTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
isg.Generate(b, tc.clause)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
igs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
igs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
igs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestDialect() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewInsertSQLGenerator("test", opts)
|
||||
igs.Equal("test", d.Dialect())
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
d2 := sqlgen.NewInsertSQLGenerator("test2", opts2)
|
||||
igs.Equal("test2", d2.Dialect())
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_UnsupportedFragment() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.InsertSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment}
|
||||
d := sqlgen.NewInsertSQLGenerator("test", opts)
|
||||
|
||||
b := sb.NewSQLBuilder(true)
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", ""))
|
||||
d.Generate(b, ic)
|
||||
igs.assertErrorSQL(b, `db: unsupported INSERT SQL fragment UpdateBeginSQLFragment`)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_empty() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", ""))
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
opts2.DefaultValuesFragment = []byte(" default values")
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts2),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_nilValues() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetCols(exp.NewColumnListExpression("a")).
|
||||
SetVals([][]interface{}{
|
||||
{nil},
|
||||
})
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a") VALUES (NULL)`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a") VALUES (?)`, isPrepared: true, args: []interface{}{nil}},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_colsAndVals() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.ValuesFragment = []byte(" values ")
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.CommaRune = ';'
|
||||
opts.PlaceHolderFragment = []byte("#")
|
||||
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetCols(exp.NewColumnListExpression("a", "b")).
|
||||
SetVals([][]interface{}{
|
||||
{"a1", "b1"},
|
||||
{"a2", "b2"},
|
||||
{"a3", "b3"},
|
||||
})
|
||||
|
||||
bic := ic.SetCols(exp.NewColumnListExpression("a", "b")).
|
||||
SetVals([][]interface{}{
|
||||
{"a1"},
|
||||
{"a2", "b2"},
|
||||
{"a3", "b3"},
|
||||
})
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {'a1'; 'b1'}; {'a2'; 'b2'}; {'a3'; 'b3'}`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {#; #}; {#; #}; {#; #}`, isPrepared: true, args: []interface{}{
|
||||
"a1", "b1", "a2", "b2", "a3", "b3",
|
||||
}},
|
||||
|
||||
insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`},
|
||||
insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withNoInto() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.ValuesFragment = []byte(" values ")
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.CommaRune = ';'
|
||||
opts.PlaceHolderFragment = []byte("#")
|
||||
|
||||
ic := exp.NewInsertClauses().
|
||||
SetCols(exp.NewColumnListExpression("a", "b")).
|
||||
SetVals([][]interface{}{
|
||||
{"a1", "b1"},
|
||||
{"a2", "b2"},
|
||||
{"a3", "b3"},
|
||||
})
|
||||
expectedErr := "db: no source found when generating insert sql"
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: ic, err: expectedErr},
|
||||
insertTestCase{clause: ic, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withRows() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.ValuesFragment = []byte(" values ")
|
||||
opts.LeftParenRune = '{'
|
||||
opts.RightParenRune = '}'
|
||||
opts.CommaRune = ';'
|
||||
opts.PlaceHolderFragment = []byte("#")
|
||||
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetRows([]interface{}{
|
||||
exp.Record{"a": "a1", "b": "b1"},
|
||||
exp.Record{"a": "a2", "b": "b2"},
|
||||
exp.Record{"a": "a3", "b": "b3"},
|
||||
})
|
||||
|
||||
bic := ic.SetRows([]interface{}{
|
||||
exp.Record{"a": "a1"},
|
||||
exp.Record{"a": "a2", "b": "b2"},
|
||||
exp.Record{"a": "a3", "b": "b3"},
|
||||
})
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {'a1'; 'b1'}; {'a2'; 'b2'}; {'a3'; 'b3'}`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" {"a"; "b"} values {#; #}; {#; #}; {#; #}`, isPrepared: true, args: []interface{}{
|
||||
"a1", "b1", "a2", "b2", "a3", "b3",
|
||||
}},
|
||||
|
||||
insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`},
|
||||
insertTestCase{clause: bic, err: `db: rows with different value length expected 1 got 2`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withEmptyRows() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetRows([]interface{}{exp.Record{}})
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" DEFAULT VALUES`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
opts2.DefaultValuesFragment = []byte(" default values")
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts2),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" default values`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withRowsAppendableExpression() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetRows([]interface{}{newTestAppendableExpression(`select * from "other"`, emptyArgs, nil, nil)})
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" select * from "other"`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" select * from "other"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withFrom() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetFrom(newTestAppendableExpression(`select c, d from test where a = 'b'`, nil, nil, nil))
|
||||
|
||||
icCols := ic.SetCols(exp.NewColumnListExpression("a", "b"))
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" select c, d from test where a = 'b'`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" select c, d from test where a = 'b'`, isPrepared: true},
|
||||
|
||||
insertTestCase{clause: icCols, sql: `INSERT INTO "test" ("a", "b") select c, d from test where a = 'b'`},
|
||||
insertTestCase{clause: icCols, sql: `INSERT INTO "test" ("a", "b") select c, d from test where a = 'b'`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_onConflict() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// make sure the fragments are used
|
||||
opts.ConflictFragment = []byte(" on conflict")
|
||||
opts.ConflictDoNothingFragment = []byte(" do nothing")
|
||||
opts.ConflictDoUpdateFragment = []byte(" do update set ")
|
||||
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetCols(exp.NewColumnListExpression("a")).
|
||||
SetVals([][]interface{}{
|
||||
{"a1"},
|
||||
})
|
||||
icDn := ic.SetOnConflict(exp.NewDoNothingConflictExpression())
|
||||
icDu := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", exp.Record{"a": "b"}))
|
||||
icAsDu := ic.SetAlias(exp.NewIdentifierExpression("", "new", "")).SetOnConflict(
|
||||
exp.NewDoUpdateConflictExpression("test", exp.Record{"a": exp.NewIdentifierExpression("", "new", "a")}),
|
||||
)
|
||||
icDoc := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("on constraint test", exp.Record{"a": "b"}))
|
||||
icDuw := ic.SetOnConflict(
|
||||
exp.NewDoUpdateConflictExpression("test", exp.Record{"a": "b"}).Where(exp.Ex{"foo": true}),
|
||||
)
|
||||
|
||||
icDuNil := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", nil))
|
||||
icDuBad := ic.SetOnConflict(exp.NewDoUpdateConflictExpression("test", true))
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: icDn, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict do nothing`},
|
||||
insertTestCase{
|
||||
clause: icDn,
|
||||
sql: `INSERT INTO "test" ("a") VALUES (?) on conflict do nothing`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1"},
|
||||
},
|
||||
|
||||
insertTestCase{clause: icDu, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b'`},
|
||||
insertTestCase{
|
||||
clause: icDu,
|
||||
sql: `INSERT INTO "test" ("a") VALUES (?) on conflict (test) do update set "a"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{clause: icAsDu, sql: `INSERT INTO "test" ("a") VALUES ('a1') AS "new" on conflict (test) do update set "a"="new"."a"`},
|
||||
insertTestCase{
|
||||
clause: icAsDu,
|
||||
sql: `INSERT INTO "test" ("a") VALUES (?) AS "new" on conflict (test) do update set "a"="new"."a"`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1"},
|
||||
},
|
||||
|
||||
insertTestCase{clause: icDoc, sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict on constraint test do update set "a"='b'`},
|
||||
insertTestCase{
|
||||
clause: icDoc,
|
||||
sql: `INSERT INTO "test" ("a") VALUES (?) on conflict on constraint test do update set "a"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{
|
||||
clause: icDuw,
|
||||
sql: `INSERT INTO "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b' WHERE ("foo" IS TRUE)`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icDuw,
|
||||
sql: `INSERT INTO "test" ("a") VALUES (?) on conflict (test) do update set "a"=? WHERE ("foo" IS TRUE)`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error()},
|
||||
insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error(), isPrepared: true},
|
||||
|
||||
insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool"},
|
||||
insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool", isPrepared: true},
|
||||
)
|
||||
opts.SupportsInsertIgnoreSyntax = true
|
||||
opts.InsertIgnoreClause = []byte("insert ignore into")
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: icDn, sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict do nothing`},
|
||||
insertTestCase{
|
||||
clause: icDn,
|
||||
sql: `insert ignore into "test" ("a") VALUES (?) on conflict do nothing`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1"},
|
||||
},
|
||||
|
||||
insertTestCase{
|
||||
clause: icDu,
|
||||
sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b'`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icDu,
|
||||
sql: `insert ignore into "test" ("a") VALUES (?) on conflict (test) do update set "a"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{
|
||||
clause: icDoc,
|
||||
sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict on constraint test do update set "a"='b'`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icDoc,
|
||||
sql: `insert ignore into "test" ("a") VALUES (?) on conflict on constraint test do update set "a"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{
|
||||
clause: icDuw,
|
||||
sql: `insert ignore into "test" ("a") VALUES ('a1') on conflict (test) do update set "a"='b' WHERE ("foo" IS TRUE)`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icDuw,
|
||||
sql: `insert ignore into "test" ("a") VALUES (?) on conflict (test) do update set "a"=? WHERE ("foo" IS TRUE)`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"a1", "b"},
|
||||
},
|
||||
|
||||
insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error()},
|
||||
insertTestCase{clause: icDuNil, err: sqlgen.ErrConflictUpdateValuesRequired.Error(), isPrepared: true},
|
||||
|
||||
insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool"},
|
||||
insertTestCase{clause: icDuBad, err: "db: unsupported update interface type bool", isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsConflictUpdateWhere = false
|
||||
expectedErr := "db: dialect does not support upsert with where clause [dialect=test]"
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: icDuw, err: expectedErr},
|
||||
insertTestCase{clause: icDuw, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withCommonTables() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.WithFragment = []byte("with ")
|
||||
opts.RecursiveFragment = []byte("recursive ")
|
||||
|
||||
tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil)
|
||||
|
||||
ic := exp.NewInsertClauses().SetInto(exp.NewIdentifierExpression("", "test_cte", ""))
|
||||
icCte1 := ic.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse))
|
||||
icCte2 := ic.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse))
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{
|
||||
clause: icCte1,
|
||||
sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icCte1,
|
||||
sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
insertTestCase{
|
||||
clause: icCte2,
|
||||
sql: `with recursive test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icCte2,
|
||||
sql: `with recursive test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
isPrepared: true,
|
||||
},
|
||||
)
|
||||
|
||||
opts.SupportsWithCTE = false
|
||||
expectedErr := "db: dialect does not support CTE WITH clause [dialect=test]"
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{clause: icCte1, err: expectedErr},
|
||||
insertTestCase{clause: icCte1, err: expectedErr, isPrepared: true},
|
||||
|
||||
insertTestCase{clause: icCte2, err: expectedErr},
|
||||
insertTestCase{clause: icCte2, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
|
||||
opts.SupportsWithCTE = true
|
||||
opts.SupportsWithCTERecursive = false
|
||||
expectedErr = "db: dialect does not support CTE WITH RECURSIVE clause [dialect=test]"
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", opts),
|
||||
insertTestCase{
|
||||
clause: icCte1,
|
||||
sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
},
|
||||
insertTestCase{
|
||||
clause: icCte1,
|
||||
sql: `with test_cte AS (select * from foo) INSERT INTO "test_cte" DEFAULT VALUES`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
insertTestCase{clause: icCte2, err: expectedErr},
|
||||
insertTestCase{clause: icCte2, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (igs *insertSQLGeneratorSuite) TestGenerate_withReturning() {
|
||||
ic := exp.NewInsertClauses().
|
||||
SetInto(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetCols(exp.NewColumnListExpression("a", "b")).
|
||||
SetVals([][]interface{}{
|
||||
{"a1", "b1"},
|
||||
}).
|
||||
SetReturning(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
igs.assertCases(
|
||||
sqlgen.NewInsertSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a", "b") VALUES ('a1', 'b1') RETURNING "a", "b"`},
|
||||
insertTestCase{clause: ic, sql: `INSERT INTO "test" ("a", "b") VALUES (?, ?) RETURNING "a", "b"`, isPrepared: true, args: []interface{}{
|
||||
"a1", "b1",
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestInsertSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(insertSQLGeneratorSuite))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import exp "git.fsdpf.net/go/db/v2/exp"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import sb "git.fsdpf.net/go/db/v2/internal/sb"
|
||||
|
||||
// DeleteSQLGenerator is an autogenerated mock type for the DeleteSQLGenerator type
|
||||
type DeleteSQLGenerator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Dialect provides a mock function with given fields:
|
||||
func (_m *DeleteSQLGenerator) Dialect() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Generate provides a mock function with given fields: b, clauses
|
||||
func (_m *DeleteSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.DeleteClauses) {
|
||||
_m.Called(b, clauses)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import exp "git.fsdpf.net/go/db/v2/exp"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import sb "git.fsdpf.net/go/db/v2/internal/sb"
|
||||
|
||||
// InsertSQLGenerator is an autogenerated mock type for the InsertSQLGenerator type
|
||||
type InsertSQLGenerator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Dialect provides a mock function with given fields:
|
||||
func (_m *InsertSQLGenerator) Dialect() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Generate provides a mock function with given fields: b, clauses
|
||||
func (_m *InsertSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.InsertClauses) {
|
||||
_m.Called(b, clauses)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import exp "git.fsdpf.net/go/db/v2/exp"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import sb "git.fsdpf.net/go/db/v2/internal/sb"
|
||||
|
||||
// SelectSQLGenerator is an autogenerated mock type for the SelectSQLGenerator type
|
||||
type SelectSQLGenerator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Dialect provides a mock function with given fields:
|
||||
func (_m *SelectSQLGenerator) Dialect() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Generate provides a mock function with given fields: b, clauses
|
||||
func (_m *SelectSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.SelectClauses) {
|
||||
_m.Called(b, clauses)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import exp "git.fsdpf.net/go/db/v2/exp"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import sb "git.fsdpf.net/go/db/v2/internal/sb"
|
||||
|
||||
// TruncateSQLGenerator is an autogenerated mock type for the TruncateSQLGenerator type
|
||||
type TruncateSQLGenerator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Dialect provides a mock function with given fields:
|
||||
func (_m *TruncateSQLGenerator) Dialect() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Generate provides a mock function with given fields: b, clauses
|
||||
func (_m *TruncateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.TruncateClauses) {
|
||||
_m.Called(b, clauses)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import exp "git.fsdpf.net/go/db/v2/exp"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import sb "git.fsdpf.net/go/db/v2/internal/sb"
|
||||
|
||||
// UpdateSQLGenerator is an autogenerated mock type for the UpdateSQLGenerator type
|
||||
type UpdateSQLGenerator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Dialect provides a mock function with given fields:
|
||||
func (_m *UpdateSQLGenerator) Dialect() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Generate provides a mock function with given fields: b, clauses
|
||||
func (_m *UpdateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.UpdateClauses) {
|
||||
_m.Called(b, clauses)
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
SelectSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, clauses exp.SelectClauses)
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
selectSQLGenerator struct {
|
||||
CommonSQLGenerator
|
||||
}
|
||||
)
|
||||
|
||||
func ErrNotSupportedJoinType(j exp.JoinExpression) error {
|
||||
return errors.New("dialect does not support %v", j.JoinType())
|
||||
}
|
||||
|
||||
func ErrJoinConditionRequired(j exp.JoinExpression) error {
|
||||
return errors.New("join condition required for conditioned join %v", j.JoinType())
|
||||
}
|
||||
|
||||
func ErrDistinctOnNotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support DISTINCT ON clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
func ErrWindowNotSupported(dialect string) error {
|
||||
return errors.New("dialect does not support WINDOW clause [dialect=%s]", dialect)
|
||||
}
|
||||
|
||||
var ErrNoWindowName = errors.New("window expresion has no valid name")
|
||||
|
||||
func NewSelectSQLGenerator(dialect string, do *SQLDialectOptions) SelectSQLGenerator {
|
||||
return &selectSQLGenerator{NewCommonSQLGenerator(dialect, do)}
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.SelectClauses) {
|
||||
for _, f := range ssg.DialectOptions().SelectSQLOrder {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
switch f {
|
||||
case CommonTableSQLFragment:
|
||||
ssg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables())
|
||||
case SelectSQLFragment:
|
||||
ssg.SelectSQL(b, clauses)
|
||||
case SelectWithLimitSQLFragment:
|
||||
ssg.SelectWithLimitSQL(b, clauses)
|
||||
case FromSQLFragment:
|
||||
ssg.FromSQL(b, clauses.From())
|
||||
case JoinSQLFragment:
|
||||
ssg.JoinSQL(b, clauses.Joins())
|
||||
case WhereSQLFragment:
|
||||
ssg.WhereSQL(b, clauses.Where())
|
||||
case GroupBySQLFragment:
|
||||
ssg.GroupBySQL(b, clauses.GroupBy())
|
||||
case HavingSQLFragment:
|
||||
ssg.HavingSQL(b, clauses.Having())
|
||||
case WindowSQLFragment:
|
||||
ssg.WindowSQL(b, clauses.Windows())
|
||||
case CompoundsSQLFragment:
|
||||
ssg.CompoundsSQL(b, clauses.Compounds())
|
||||
case OrderSQLFragment:
|
||||
ssg.OrderSQL(b, clauses.Order())
|
||||
case OrderWithOffsetFetchSQLFragment:
|
||||
ssg.OrderWithOffsetFetchSQL(b, clauses.Order(), clauses.Offset(), clauses.Limit())
|
||||
case LimitSQLFragment:
|
||||
ssg.LimitSQL(b, clauses.Limit())
|
||||
case OffsetSQLFragment:
|
||||
ssg.OffsetSQL(b, clauses.Offset())
|
||||
case ForSQLFragment:
|
||||
ssg.ForSQL(b, clauses.Lock())
|
||||
default:
|
||||
b.SetError(ErrNotSupportedFragment("SELECT", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) selectSQLCommon(b sb.SQLBuilder, clauses exp.SelectClauses) {
|
||||
dc := clauses.Distinct()
|
||||
if dc != nil {
|
||||
b.Write(ssg.DialectOptions().DistinctFragment)
|
||||
if !dc.IsEmpty() {
|
||||
if ssg.DialectOptions().SupportsDistinctOn {
|
||||
b.Write(ssg.DialectOptions().OnFragment).WriteRunes(ssg.DialectOptions().LeftParenRune)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, dc)
|
||||
b.WriteRunes(ssg.DialectOptions().RightParenRune, ssg.DialectOptions().SpaceRune)
|
||||
} else {
|
||||
b.SetError(ErrDistinctOnNotSupported(ssg.Dialect()))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b.WriteRunes(ssg.DialectOptions().SpaceRune)
|
||||
}
|
||||
}
|
||||
|
||||
if cols := clauses.Select(); clauses.IsDefaultSelect() || len(cols.Columns()) == 0 {
|
||||
b.WriteRunes(ssg.DialectOptions().StarRune)
|
||||
} else {
|
||||
ssg.ExpressionSQLGenerator().Generate(b, cols)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the SELECT clause and columns to a sql statement
|
||||
func (ssg *selectSQLGenerator) SelectSQL(b sb.SQLBuilder, clauses exp.SelectClauses) {
|
||||
b.Write(ssg.DialectOptions().SelectClause).WriteRunes(ssg.DialectOptions().SpaceRune)
|
||||
ssg.selectSQLCommon(b, clauses)
|
||||
}
|
||||
|
||||
// Adds the SELECT clause along with LIMIT to a SQL statement (e.g. MSSQL dialect: SELECT TOP 10 ...)
|
||||
func (ssg *selectSQLGenerator) SelectWithLimitSQL(b sb.SQLBuilder, clauses exp.SelectClauses) {
|
||||
b.Write(ssg.DialectOptions().SelectClause).WriteRunes(ssg.DialectOptions().SpaceRune)
|
||||
if clauses.Offset() == 0 && clauses.Limit() != nil {
|
||||
ssg.LimitSQL(b, clauses.Limit())
|
||||
b.WriteRunes(ssg.DialectOptions().SpaceRune)
|
||||
}
|
||||
ssg.selectSQLCommon(b, clauses)
|
||||
}
|
||||
|
||||
// Generates the JOIN clauses for an SQL statement
|
||||
func (ssg *selectSQLGenerator) JoinSQL(b sb.SQLBuilder, joins exp.JoinExpressions) {
|
||||
if len(joins) > 0 {
|
||||
for _, j := range joins {
|
||||
joinType, ok := ssg.DialectOptions().JoinTypeLookup[j.JoinType()]
|
||||
if !ok {
|
||||
b.SetError(ErrNotSupportedJoinType(j))
|
||||
return
|
||||
}
|
||||
b.Write(joinType)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, j.Table())
|
||||
if t, ok := j.(exp.ConditionedJoinExpression); ok {
|
||||
if t.IsConditionEmpty() {
|
||||
b.SetError(ErrJoinConditionRequired(j))
|
||||
return
|
||||
}
|
||||
ssg.joinConditionSQL(b, t.Condition())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the GROUP BY clause for an SQL statement
|
||||
func (ssg *selectSQLGenerator) GroupBySQL(b sb.SQLBuilder, groupBy exp.ColumnListExpression) {
|
||||
if groupBy != nil && len(groupBy.Columns()) > 0 {
|
||||
b.Write(ssg.DialectOptions().GroupByFragment)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, groupBy)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the HAVING clause for an SQL statement
|
||||
func (ssg *selectSQLGenerator) HavingSQL(b sb.SQLBuilder, having exp.ExpressionList) {
|
||||
if having != nil && len(having.Expressions()) > 0 {
|
||||
b.Write(ssg.DialectOptions().HavingFragment)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, having)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the OFFSET clause for an SQL statement
|
||||
func (ssg *selectSQLGenerator) OffsetSQL(b sb.SQLBuilder, offset uint) {
|
||||
if offset > 0 {
|
||||
b.Write(ssg.DialectOptions().OffsetFragment)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, offset)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the compound sql clause for an SQL statement (e.g. UNION, INTERSECT)
|
||||
func (ssg *selectSQLGenerator) CompoundsSQL(b sb.SQLBuilder, compounds []exp.CompoundExpression) {
|
||||
for _, compound := range compounds {
|
||||
ssg.ExpressionSQLGenerator().Generate(b, compound)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates the FOR (aka "locking") clause for an SQL statement
|
||||
func (ssg *selectSQLGenerator) ForSQL(b sb.SQLBuilder, lockingClause exp.Lock) {
|
||||
if lockingClause == nil {
|
||||
return
|
||||
}
|
||||
switch lockingClause.Strength() {
|
||||
case exp.ForNolock:
|
||||
return
|
||||
case exp.ForUpdate:
|
||||
b.Write(ssg.DialectOptions().ForUpdateFragment)
|
||||
case exp.ForNoKeyUpdate:
|
||||
b.Write(ssg.DialectOptions().ForNoKeyUpdateFragment)
|
||||
case exp.ForShare:
|
||||
b.Write(ssg.DialectOptions().ForShareFragment)
|
||||
case exp.ForKeyShare:
|
||||
b.Write(ssg.DialectOptions().ForKeyShareFragment)
|
||||
}
|
||||
|
||||
of := lockingClause.Of()
|
||||
if ofLen := len(of); ofLen > 0 {
|
||||
if ofFragment := ssg.DialectOptions().OfFragment; len(ofFragment) > 0 {
|
||||
b.Write(ofFragment)
|
||||
for i, table := range of {
|
||||
ssg.ExpressionSQLGenerator().Generate(b, table)
|
||||
if i < ofLen-1 {
|
||||
b.WriteRunes(ssg.DialectOptions().CommaRune, ssg.DialectOptions().SpaceRune)
|
||||
}
|
||||
}
|
||||
b.WriteRunes(ssg.DialectOptions().SpaceRune)
|
||||
}
|
||||
}
|
||||
|
||||
// the WAIT case is the default in Postgres, and is what you get if you don't specify NOWAIT or
|
||||
// SKIP LOCKED. There's no special syntax for it in PG, so we don't do anything for it here
|
||||
switch lockingClause.WaitOption() {
|
||||
case exp.Wait:
|
||||
return
|
||||
case exp.NoWait:
|
||||
b.Write(ssg.DialectOptions().NowaitFragment)
|
||||
case exp.SkipLocked:
|
||||
b.Write(ssg.DialectOptions().SkipLockedFragment)
|
||||
}
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) WindowSQL(b sb.SQLBuilder, windows []exp.WindowExpression) {
|
||||
weLen := len(windows)
|
||||
if weLen == 0 {
|
||||
return
|
||||
}
|
||||
if !ssg.DialectOptions().SupportsWindowFunction {
|
||||
b.SetError(ErrWindowNotSupported(ssg.Dialect()))
|
||||
return
|
||||
}
|
||||
b.Write(ssg.DialectOptions().WindowFragment)
|
||||
for i, we := range windows {
|
||||
if !we.HasName() {
|
||||
b.SetError(ErrNoWindowName)
|
||||
}
|
||||
ssg.ExpressionSQLGenerator().Generate(b, we)
|
||||
if i < weLen-1 {
|
||||
b.WriteRunes(ssg.DialectOptions().CommaRune, ssg.DialectOptions().SpaceRune)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) joinConditionSQL(b sb.SQLBuilder, jc exp.JoinCondition) {
|
||||
switch t := jc.(type) {
|
||||
case exp.JoinOnCondition:
|
||||
ssg.joinOnConditionSQL(b, t)
|
||||
case exp.JoinUsingCondition:
|
||||
ssg.joinUsingConditionSQL(b, t)
|
||||
}
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) joinUsingConditionSQL(b sb.SQLBuilder, jc exp.JoinUsingCondition) {
|
||||
b.Write(ssg.DialectOptions().UsingFragment).
|
||||
WriteRunes(ssg.DialectOptions().LeftParenRune)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, jc.Using())
|
||||
b.WriteRunes(ssg.DialectOptions().RightParenRune)
|
||||
}
|
||||
|
||||
func (ssg *selectSQLGenerator) joinOnConditionSQL(b sb.SQLBuilder, jc exp.JoinOnCondition) {
|
||||
b.Write(ssg.DialectOptions().OnFragment)
|
||||
ssg.ExpressionSQLGenerator().Generate(b, jc.On())
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
dbv2 "git.fsdpf.net/go/db/v2"
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
selectTestCase struct {
|
||||
clause exp.SelectClauses
|
||||
sql string
|
||||
isPrepared bool
|
||||
args []interface{}
|
||||
err string
|
||||
}
|
||||
selectSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) assertCases(ssg sqlgen.SelectSQLGenerator, testCases ...selectTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
ssg.Generate(b, tc.clause)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
ssgs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
ssgs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
ssgs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestDialect() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewSelectSQLGenerator("test", opts)
|
||||
ssgs.Equal("test", d.Dialect())
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
d2 := sqlgen.NewSelectSQLGenerator("test2", opts2)
|
||||
ssgs.Equal("test2", d2.Dialect())
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SelectClause = []byte("select")
|
||||
opts.StarRune = '#'
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
scWithCols := sc.SetSelect(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
ident := exp.NewIdentifierExpression("", "", "a")
|
||||
scWithBooExpAliased := sc.SetSelect(
|
||||
exp.NewColumnListExpression(
|
||||
ident.Eq(1).As("x"),
|
||||
ident.IsNull().As("y"),
|
||||
),
|
||||
)
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, sql: `select # FROM "test"`},
|
||||
selectTestCase{clause: sc, sql: `select # FROM "test"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scWithCols, sql: `select "a", "b" FROM "test"`},
|
||||
selectTestCase{clause: scWithCols, sql: `select "a", "b" FROM "test"`, isPrepared: true},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWithBooExpAliased,
|
||||
sql: `select ("a" = 1) AS "x", ("a" IS NULL) AS "y" FROM "test"`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWithBooExpAliased,
|
||||
sql: `select ("a" = ?) AS "x", ("a" IS NULL) AS "y" FROM "test"`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{int64(1)},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_UnsupportedFragment() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SelectSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment}
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
expectedErr := "db: unsupported SELECT SQL fragment InsertBeingSQLFragment"
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, err: expectedErr},
|
||||
selectTestCase{clause: sc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_WithErroredBuilder() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SelectSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment}
|
||||
d := sqlgen.NewSelectSQLGenerator("test", opts)
|
||||
|
||||
b := sb.NewSQLBuilder(true).SetError(errors.New("test error"))
|
||||
c := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
d.Generate(b, c)
|
||||
ssgs.assertErrorSQL(b, `db: test error`)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withSelectedColumns() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// make sure the fragments are used
|
||||
opts.SelectClause = []byte("select")
|
||||
opts.StarRune = '#'
|
||||
opts.SupportsDistinctOn = true
|
||||
|
||||
sc := exp.NewSelectClauses()
|
||||
scCols := sc.SetSelect(exp.NewColumnListExpression("a", "b"))
|
||||
scFuncs := sc.SetSelect(exp.NewColumnListExpression(
|
||||
exp.NewSQLFunctionExpression("COUNT", exp.Star()),
|
||||
exp.NewSQLFunctionExpression("RANK"),
|
||||
))
|
||||
|
||||
we := exp.NewWindowExpression(
|
||||
nil,
|
||||
nil,
|
||||
exp.NewColumnListExpression("a", "b"),
|
||||
exp.NewOrderedColumnList(exp.ParseIdentifier("c").Asc()),
|
||||
)
|
||||
scFuncsPartition := sc.SetSelect(exp.NewColumnListExpression(
|
||||
exp.NewSQLFunctionExpression("COUNT", exp.Star()).Over(we),
|
||||
exp.NewSQLFunctionExpression("RANK").Over(we.Inherit("w")),
|
||||
))
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, sql: `select #`},
|
||||
selectTestCase{clause: sc, sql: `select #`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scCols, sql: `select "a", "b"`},
|
||||
selectTestCase{clause: scCols, sql: `select "a", "b"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFuncs, sql: `select COUNT(*), RANK()`},
|
||||
selectTestCase{clause: scFuncs, sql: `select COUNT(*), RANK()`, isPrepared: true},
|
||||
|
||||
selectTestCase{
|
||||
clause: scFuncsPartition,
|
||||
sql: `select COUNT(*) OVER (PARTITION BY "a", "b" ORDER BY "c" ASC), RANK() OVER ("w" PARTITION BY "a", "b" ORDER BY "c" ASC)`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scFuncsPartition,
|
||||
sql: `select COUNT(*) OVER (PARTITION BY "a", "b" ORDER BY "c" ASC), RANK() OVER ("w" PARTITION BY "a", "b" ORDER BY "c" ASC)`,
|
||||
isPrepared: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withDistinct() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// make sure the fragments are used
|
||||
opts.SelectClause = []byte("select")
|
||||
opts.StarRune = '#'
|
||||
opts.DistinctFragment = []byte("distinct")
|
||||
opts.OnFragment = []byte(" on ")
|
||||
opts.SupportsDistinctOn = true
|
||||
|
||||
sc := exp.NewSelectClauses().SetDistinct(exp.NewColumnListExpression())
|
||||
scDistinctOn := sc.SetDistinct(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, sql: `select distinct #`},
|
||||
selectTestCase{clause: sc, sql: `select distinct #`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scDistinctOn, sql: `select distinct on ("a", "b") #`},
|
||||
selectTestCase{clause: scDistinctOn, sql: `select distinct on ("a", "b") #`, isPrepared: true},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsDistinctOn = false
|
||||
expectedErr := "db: dialect does not support DISTINCT ON clause [dialect=test]"
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, sql: `SELECT DISTINCT *`},
|
||||
selectTestCase{clause: sc, sql: `SELECT DISTINCT *`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scDistinctOn, err: expectedErr},
|
||||
selectTestCase{clause: scDistinctOn, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withFromSQL() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.FromFragment = []byte(" from")
|
||||
|
||||
sc := exp.NewSelectClauses()
|
||||
scFrom := sc.SetFrom(exp.NewColumnListExpression("a", "b"))
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc, sql: `SELECT *`},
|
||||
selectTestCase{clause: sc, sql: `SELECT *`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFrom, sql: `SELECT * from "a", "b"`},
|
||||
selectTestCase{clause: scFrom, sql: `SELECT * from "a", "b"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withJoin() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// override fragements to make sure dialect is used
|
||||
opts.UsingFragment = []byte(" using ")
|
||||
opts.OnFragment = []byte(" on ")
|
||||
opts.JoinTypeLookup = map[exp.JoinType][]byte{
|
||||
exp.LeftJoinType: []byte(" left join "),
|
||||
exp.NaturalJoinType: []byte(" natural join "),
|
||||
}
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
ti := exp.NewIdentifierExpression("", "test2", "")
|
||||
uj := exp.NewUnConditionedJoinExpression(exp.NaturalJoinType, ti)
|
||||
cjo := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinOnCondition(exp.Ex{"a": "foo"}))
|
||||
cju := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinUsingCondition("a"))
|
||||
rj := exp.NewConditionedJoinExpression(exp.RightJoinType, ti, exp.NewJoinUsingCondition(exp.NewIdentifierExpression("", "", "a")))
|
||||
badJoin := exp.NewConditionedJoinExpression(exp.LeftJoinType, ti, exp.NewJoinUsingCondition())
|
||||
|
||||
expectedRjError := "db: dialect does not support RightJoinType"
|
||||
expectedJoinCondError := "db: join condition required for conditioned join LeftJoinType"
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: sc.JoinsAppend(uj), sql: `SELECT * FROM "test" natural join "test2"`},
|
||||
selectTestCase{clause: sc.JoinsAppend(uj), sql: `SELECT * FROM "test" natural join "test2"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: sc.JoinsAppend(cjo), sql: `SELECT * FROM "test" left join "test2" on ("a" = 'foo')`},
|
||||
selectTestCase{
|
||||
clause: sc.JoinsAppend(cjo),
|
||||
sql: `SELECT * FROM "test" left join "test2" on ("a" = ?)`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"foo"},
|
||||
},
|
||||
|
||||
selectTestCase{clause: sc.JoinsAppend(cju), sql: `SELECT * FROM "test" left join "test2" using ("a")`},
|
||||
selectTestCase{clause: sc.JoinsAppend(cju), sql: `SELECT * FROM "test" left join "test2" using ("a")`, isPrepared: true},
|
||||
|
||||
selectTestCase{
|
||||
clause: sc.JoinsAppend(uj).JoinsAppend(cjo).JoinsAppend(cju),
|
||||
sql: `SELECT * FROM "test" natural join "test2" left join "test2" on ("a" = 'foo') left join "test2" using ("a")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: sc.JoinsAppend(uj).JoinsAppend(cjo).JoinsAppend(cju),
|
||||
sql: `SELECT * FROM "test" natural join "test2" left join "test2" on ("a" = ?) left join "test2" using ("a")`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"foo"},
|
||||
},
|
||||
|
||||
selectTestCase{clause: sc.JoinsAppend(rj), err: expectedRjError},
|
||||
selectTestCase{clause: sc.JoinsAppend(rj), err: expectedRjError, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: sc.JoinsAppend(badJoin), err: expectedJoinCondError},
|
||||
selectTestCase{clause: sc.JoinsAppend(badJoin), err: expectedJoinCondError, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withWhere() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.WhereFragment = []byte(" where ")
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
w := exp.Ex{"a": "b"}
|
||||
w2 := exp.Ex{"b": "c"}
|
||||
scWhere1 := sc.WhereAppend(w)
|
||||
scWhere2 := sc.WhereAppend(w, w2)
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: scWhere1, sql: `SELECT * FROM "test" where ("a" = 'b')`},
|
||||
selectTestCase{clause: scWhere1, sql: `SELECT * FROM "test" where ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
selectTestCase{clause: scWhere2, sql: `SELECT * FROM "test" where (("a" = 'b') AND ("b" = 'c'))`},
|
||||
selectTestCase{
|
||||
clause: scWhere2,
|
||||
sql: `SELECT * FROM "test" where (("a" = ?) AND ("b" = ?))`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withGroupBy() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.GroupByFragment = []byte(" group by ")
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
scGroup := sc.SetGroupBy(exp.NewColumnListExpression("a"))
|
||||
scGroupMulti := sc.SetGroupBy(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: scGroup, sql: `SELECT * FROM "test" group by "a"`},
|
||||
selectTestCase{clause: scGroup, sql: `SELECT * FROM "test" group by "a"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scGroupMulti, sql: `SELECT * FROM "test" group by "a", "b"`},
|
||||
selectTestCase{clause: scGroupMulti, sql: `SELECT * FROM "test" group by "a", "b"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withHaving() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.HavingFragment = []byte(" having ")
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
w := exp.Ex{"a": "b"}
|
||||
w2 := exp.Ex{"b": "c"}
|
||||
scHaving1 := sc.HavingAppend(w)
|
||||
scHaving2 := sc.HavingAppend(w, w2)
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: scHaving1, sql: `SELECT * FROM "test" having ("a" = 'b')`},
|
||||
selectTestCase{clause: scHaving1, sql: `SELECT * FROM "test" having ("a" = ?)`, isPrepared: true, args: []interface{}{"b"}},
|
||||
|
||||
selectTestCase{clause: scHaving2, sql: `SELECT * FROM "test" having (("a" = 'b') AND ("b" = 'c'))`},
|
||||
selectTestCase{
|
||||
clause: scHaving2,
|
||||
sql: `SELECT * FROM "test" having (("a" = ?) AND ("b" = ?))`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withWindow() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.WindowFragment = []byte(" window ")
|
||||
opts.WindowPartitionByFragment = []byte("partition by ")
|
||||
opts.WindowOrderByFragment = []byte("order by ")
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
we1 := exp.NewWindowExpression(
|
||||
exp.NewIdentifierExpression("", "", "w"),
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
wePartitionBy := we1.PartitionBy("a", "b")
|
||||
weOrderBy := we1.OrderBy("a", "b")
|
||||
|
||||
weOrderAndPartitionBy := we1.PartitionBy("a", "b").OrderBy("a", "b")
|
||||
|
||||
weInherits := exp.NewWindowExpression(
|
||||
exp.NewIdentifierExpression("", "", "w2"),
|
||||
exp.NewIdentifierExpression("", "", "w"),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
weInheritsPartitionBy := weInherits.PartitionBy("c", "d")
|
||||
weInheritsOrderBy := weInherits.OrderBy("c", "d")
|
||||
|
||||
weInheritsOrderAndPartitionBy := weInherits.PartitionBy("c", "d").OrderBy("c", "d")
|
||||
|
||||
scNoName := sc.WindowsAppend(exp.NewWindowExpression(nil, nil, nil, nil))
|
||||
|
||||
scWindow1 := sc.WindowsAppend(we1)
|
||||
scWindow2 := sc.WindowsAppend(wePartitionBy)
|
||||
scWindow3 := sc.WindowsAppend(weOrderBy)
|
||||
scWindow4 := sc.WindowsAppend(weOrderAndPartitionBy)
|
||||
|
||||
scWindow5 := sc.WindowsAppend(we1, weInherits)
|
||||
scWindow6 := sc.WindowsAppend(we1, weInheritsPartitionBy)
|
||||
scWindow7 := sc.WindowsAppend(we1, weInheritsOrderBy)
|
||||
scWindow8 := sc.WindowsAppend(we1, weInheritsOrderAndPartitionBy)
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
|
||||
selectTestCase{clause: scNoName, err: sqlgen.ErrNoWindowName.Error()},
|
||||
selectTestCase{clause: scNoName, err: sqlgen.ErrNoWindowName.Error(), isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scWindow1, sql: `SELECT * FROM "test" window "w" AS ()`},
|
||||
selectTestCase{clause: scWindow1, sql: `SELECT * FROM "test" window "w" AS ()`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scWindow2, sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b")`},
|
||||
selectTestCase{
|
||||
clause: scWindow2,
|
||||
sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{clause: scWindow3, sql: `SELECT * FROM "test" window "w" AS (order by "a", "b")`},
|
||||
selectTestCase{
|
||||
clause: scWindow3,
|
||||
sql: `SELECT * FROM "test" window "w" AS (order by "a", "b")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWindow4,
|
||||
sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b" order by "a", "b")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWindow4,
|
||||
sql: `SELECT * FROM "test" window "w" AS (partition by "a", "b" order by "a", "b")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWindow5,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWindow5,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWindow6,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWindow6,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWindow7,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" order by "c", "d")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWindow7,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" order by "c", "d")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
|
||||
selectTestCase{
|
||||
clause: scWindow8,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d" order by "c", "d")`,
|
||||
},
|
||||
selectTestCase{
|
||||
clause: scWindow8,
|
||||
sql: `SELECT * FROM "test" window "w" AS (), "w2" AS ("w" partition by "c", "d" order by "c", "d")`,
|
||||
isPrepared: true,
|
||||
},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsWindowFunction = false
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
|
||||
selectTestCase{clause: scWindow1, err: sqlgen.ErrWindowNotSupported("test").Error()},
|
||||
selectTestCase{clause: scWindow1, err: sqlgen.ErrWindowNotSupported("test").Error(), isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withOrder() {
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).
|
||||
SetOrder(
|
||||
exp.NewIdentifierExpression("", "", "a").Asc(),
|
||||
exp.NewIdentifierExpression("", "", "b").Desc(),
|
||||
)
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" ORDER BY "a" ASC, "b" DESC`},
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" ORDER BY "a" ASC, "b" DESC`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withLimit() {
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).
|
||||
SetLimit(10)
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" LIMIT 10`},
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" LIMIT ?`, isPrepared: true, args: []interface{}{int64(10)}},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withOffset() {
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).
|
||||
SetOffset(10)
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" OFFSET 10`},
|
||||
selectTestCase{clause: sc, sql: `SELECT * FROM "test" OFFSET ?`, isPrepared: true, args: []interface{}{int64(10)}},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCommonTables() {
|
||||
tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil)
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test_cte"))
|
||||
scCte1 := sc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse))
|
||||
scCte2 := sc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse))
|
||||
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
selectTestCase{clause: scCte1, sql: `WITH test_cte AS (select * from foo) SELECT * FROM "test_cte"`},
|
||||
selectTestCase{clause: scCte1, sql: `WITH test_cte AS (select * from foo) SELECT * FROM "test_cte"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scCte2, sql: `WITH RECURSIVE test_cte AS (select * from foo) SELECT * FROM "test_cte"`},
|
||||
selectTestCase{clause: scCte2, sql: `WITH RECURSIVE test_cte AS (select * from foo) SELECT * FROM "test_cte"`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestGenerate_withCompounds() {
|
||||
tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil)
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test")).
|
||||
CompoundsAppend(exp.NewCompoundExpression(exp.UnionCompoundType, tse)).
|
||||
CompoundsAppend(exp.NewCompoundExpression(exp.IntersectCompoundType, tse))
|
||||
|
||||
expectedSQL := `SELECT * FROM "test" UNION (select * from foo) INTERSECT (select * from foo)`
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
selectTestCase{clause: sc, sql: expectedSQL},
|
||||
selectTestCase{clause: sc, sql: expectedSQL, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (ssgs *selectSQLGeneratorSuite) TestToSelectSQL_withFor() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.ForUpdateFragment = []byte(" for update ")
|
||||
opts.ForNoKeyUpdateFragment = []byte(" for no key update ")
|
||||
opts.ForShareFragment = []byte(" for share ")
|
||||
opts.ForKeyShareFragment = []byte(" for key share ")
|
||||
opts.OfFragment = []byte("of ")
|
||||
opts.NowaitFragment = []byte("nowait")
|
||||
opts.SkipLockedFragment = []byte("skip locked")
|
||||
|
||||
sc := exp.NewSelectClauses().SetFrom(exp.NewColumnListExpression("test"))
|
||||
scFnW := sc.SetLock(exp.NewLock(exp.ForNolock, exp.Wait))
|
||||
scFnNw := sc.SetLock(exp.NewLock(exp.ForNolock, exp.NoWait))
|
||||
scFnSl := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked))
|
||||
scFnSlOf := sc.SetLock(exp.NewLock(exp.ForNolock, exp.SkipLocked, dbv2.T("my_table")))
|
||||
|
||||
scFsW := sc.SetLock(exp.NewLock(exp.ForShare, exp.Wait))
|
||||
scFsNw := sc.SetLock(exp.NewLock(exp.ForShare, exp.NoWait))
|
||||
scFsSl := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked))
|
||||
scFsSlOf := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, dbv2.T("my_table")))
|
||||
scFsSlOfMulti := sc.SetLock(exp.NewLock(exp.ForShare, exp.SkipLocked, dbv2.T("my_table"), dbv2.T("table2")))
|
||||
|
||||
scFksW := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.Wait))
|
||||
scFksNw := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.NoWait))
|
||||
scFksSl := sc.SetLock(exp.NewLock(exp.ForKeyShare, exp.SkipLocked))
|
||||
|
||||
scFuW := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.Wait))
|
||||
scFuNw := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.NoWait))
|
||||
scFuSl := sc.SetLock(exp.NewLock(exp.ForUpdate, exp.SkipLocked))
|
||||
|
||||
scFkuW := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.Wait))
|
||||
scFkuNw := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.NoWait))
|
||||
scFkuSl := sc.SetLock(exp.NewLock(exp.ForNoKeyUpdate, exp.SkipLocked))
|
||||
ssgs.assertCases(
|
||||
sqlgen.NewSelectSQLGenerator("test", opts),
|
||||
selectTestCase{clause: scFnW, sql: `SELECT * FROM "test"`},
|
||||
selectTestCase{clause: scFnW, sql: `SELECT * FROM "test"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFnNw, sql: `SELECT * FROM "test"`},
|
||||
selectTestCase{clause: scFnNw, sql: `SELECT * FROM "test"`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`},
|
||||
selectTestCase{clause: scFnSl, sql: `SELECT * FROM "test"`, isPrepared: true},
|
||||
selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`},
|
||||
selectTestCase{clause: scFnSlOf, sql: `SELECT * FROM "test"`, isPrepared: true, args: []interface{}{}},
|
||||
|
||||
selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `},
|
||||
selectTestCase{clause: scFsW, sql: `SELECT * FROM "test" for share `, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFsNw, sql: `SELECT * FROM "test" for share nowait`},
|
||||
selectTestCase{clause: scFsNw, sql: `SELECT * FROM "test" for share nowait`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`},
|
||||
selectTestCase{clause: scFsSl, sql: `SELECT * FROM "test" for share skip locked`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`},
|
||||
selectTestCase{clause: scFsSlOf, sql: `SELECT * FROM "test" for share of "my_table" skip locked`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`},
|
||||
selectTestCase{clause: scFsSlOfMulti, sql: `SELECT * FROM "test" for share of "my_table", "table2" skip locked`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `},
|
||||
selectTestCase{clause: scFksW, sql: `SELECT * FROM "test" for key share `, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFksNw, sql: `SELECT * FROM "test" for key share nowait`},
|
||||
selectTestCase{clause: scFksNw, sql: `SELECT * FROM "test" for key share nowait`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFksSl, sql: `SELECT * FROM "test" for key share skip locked`},
|
||||
selectTestCase{clause: scFksSl, sql: `SELECT * FROM "test" for key share skip locked`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFuW, sql: `SELECT * FROM "test" for update `},
|
||||
selectTestCase{clause: scFuW, sql: `SELECT * FROM "test" for update `, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFuNw, sql: `SELECT * FROM "test" for update nowait`},
|
||||
selectTestCase{clause: scFuNw, sql: `SELECT * FROM "test" for update nowait`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFuSl, sql: `SELECT * FROM "test" for update skip locked`},
|
||||
selectTestCase{clause: scFuSl, sql: `SELECT * FROM "test" for update skip locked`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFkuW, sql: `SELECT * FROM "test" for no key update `},
|
||||
selectTestCase{clause: scFkuW, sql: `SELECT * FROM "test" for no key update `, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFkuNw, sql: `SELECT * FROM "test" for no key update nowait`},
|
||||
selectTestCase{clause: scFkuNw, sql: `SELECT * FROM "test" for no key update nowait`, isPrepared: true},
|
||||
|
||||
selectTestCase{clause: scFkuSl, sql: `SELECT * FROM "test" for no key update skip locked`},
|
||||
selectTestCase{clause: scFkuSl, sql: `SELECT * FROM "test" for no key update skip locked`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func TestSelectSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(selectSQLGeneratorSuite))
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
)
|
||||
|
||||
type (
|
||||
SQLFragmentType int
|
||||
SQLDialectOptions struct {
|
||||
// Set to true if the dialect supports ORDER BY expressions in DELETE statements (DEFAULT=false)
|
||||
SupportsOrderByOnDelete bool
|
||||
// Set to true if the dialect supports table hint for DELETE statements (DELETE t FROM t ...), DEFAULT=false
|
||||
SupportsDeleteTableHint bool
|
||||
// Set to true if the dialect supports ORDER BY expressions in UPDATE statements (DEFAULT=false)
|
||||
SupportsOrderByOnUpdate bool
|
||||
// Set to true if the dialect supports LIMIT expressions in DELETE statements (DEFAULT=false)
|
||||
SupportsLimitOnDelete bool
|
||||
// Set to true if the dialect supports LIMIT expressions in UPDATE statements (DEFAULT=false)
|
||||
SupportsLimitOnUpdate bool
|
||||
// Set to true if the dialect supports RETURN expressions (DEFAULT=true)
|
||||
SupportsReturn bool
|
||||
// Set to true if the dialect supports Conflict Target (DEFAULT=true)
|
||||
SupportsConflictTarget bool
|
||||
// Set to true if the dialect supports Conflict Target (DEFAULT=true)
|
||||
SupportsConflictUpdateWhere bool
|
||||
// Set to true if the dialect supports Insert Ignore syntax (DEFAULT=false)
|
||||
SupportsInsertIgnoreSyntax bool
|
||||
// Set to true if the dialect supports Common Table Expressions (DEFAULT=true)
|
||||
SupportsWithCTE bool
|
||||
// Set to true if the dialect supports recursive Common Table Expressions (DEFAULT=true)
|
||||
SupportsWithCTERecursive bool
|
||||
// Set to true if multiple tables are supported in UPDATE statement. (DEFAULT=true)
|
||||
SupportsMultipleUpdateTables bool
|
||||
// Set to true if DISTINCT ON is supported (DEFAULT=true)
|
||||
SupportsDistinctOn bool
|
||||
// Set to true if LATERAL queries are supported (DEFAULT=true)
|
||||
SupportsLateral bool
|
||||
// Set to false if the dialect does not require expressions to be wrapped in parens (DEFAULT=true)
|
||||
WrapCompoundsInParens bool
|
||||
|
||||
// Set to true if window function are supported in SELECT statement. (DEFAULT=true)
|
||||
SupportsWindowFunction bool
|
||||
|
||||
// Set to true if the dialect requires join tables in UPDATE to be in a FROM clause (DEFAULT=true).
|
||||
UseFromClauseForMultipleUpdateTables bool
|
||||
|
||||
// Surround LIMIT parameter with parentheses, like in MSSQL: SELECT TOP (10) ...
|
||||
SurroundLimitWithParentheses bool
|
||||
|
||||
// The UPDATE fragment to use when generating sql. (DEFAULT=[]byte("UPDATE"))
|
||||
UpdateClause []byte
|
||||
// The INSERT fragment to use when generating sql. (DEFAULT=[]byte("INSERT INTO"))
|
||||
InsertClause []byte
|
||||
// The INSERT IGNORE INTO fragment to use when generating sql. (DEFAULT=[]byte("INSERT IGNORE INTO"))
|
||||
InsertIgnoreClause []byte
|
||||
// The SELECT fragment to use when generating sql. (DEFAULT=[]byte("SELECT"))
|
||||
SelectClause []byte
|
||||
// The DELETE fragment to use when generating sql. (DEFAULT=[]byte("DELETE"))
|
||||
DeleteClause []byte
|
||||
// The TRUNCATE fragment to use when generating sql. (DEFAULT=[]byte("TRUNCATE"))
|
||||
TruncateClause []byte
|
||||
// The WITH fragment to use when generating sql. (DEFAULT=[]byte("WITH "))
|
||||
WithFragment []byte
|
||||
// The RECURSIVE fragment to use when generating sql (after WITH). (DEFAULT=[]byte("RECURSIVE "))
|
||||
RecursiveFragment []byte
|
||||
// The CASCADE fragment to use when generating sql. (DEFAULT=[]byte(" CASCADE"))
|
||||
CascadeFragment []byte
|
||||
// The RESTRICT fragment to use when generating sql. (DEFAULT=[]byte(" RESTRICT"))
|
||||
RestrictFragment []byte
|
||||
// The SQL fragment to use when generating insert sql and using
|
||||
// DEFAULT VALUES (e.g. postgres="DEFAULT VALUES", mysql="", sqlite3=""). (DEFAULT=[]byte(" DEFAULT VALUES"))
|
||||
DefaultValuesFragment []byte
|
||||
// The SQL fragment to use when generating insert sql and listing columns using a VALUES clause
|
||||
// (DEFAULT=[]byte(" VALUES "))
|
||||
ValuesFragment []byte
|
||||
// The SQL fragment to use when generating truncate sql and using the IDENTITY clause
|
||||
// (DEFAULT=[]byte(" IDENTITY"))
|
||||
IdentityFragment []byte
|
||||
// The SQL fragment to use when generating update sql and using the SET clause (DEFAULT=[]byte(" SET "))
|
||||
SetFragment []byte
|
||||
// The SQL DISTINCT keyword (DEFAULT=[]byte(" DISTINCT "))
|
||||
DistinctFragment []byte
|
||||
// The SQL RETURNING clause (DEFAULT=[]byte(" RETURNING "))
|
||||
ReturningFragment []byte
|
||||
// The SQL FROM clause fragment (DEFAULT=[]byte(" FROM"))
|
||||
FromFragment []byte
|
||||
// The SQL USING join clause fragment (DEFAULT=[]byte(" USING "))
|
||||
UsingFragment []byte
|
||||
// The SQL ON join clause fragment (DEFAULT=[]byte(" ON "))
|
||||
OnFragment []byte
|
||||
// The SQL WHERE clause fragment (DEFAULT=[]byte(" WHERE "))
|
||||
WhereFragment []byte
|
||||
// The SQL GROUP BY clause fragment(DEFAULT=[]byte(" GROUP BY "))
|
||||
GroupByFragment []byte
|
||||
// The SQL HAVING clause fragment(DEFAULT=[]byte(" HAVING "))
|
||||
HavingFragment []byte
|
||||
// The SQL WINDOW clause fragment(DEFAULT=[]byte(" WINDOW "))
|
||||
WindowFragment []byte
|
||||
// The SQL WINDOW clause PARTITION BY fragment(DEFAULT=[]byte("PARTITION BY "))
|
||||
WindowPartitionByFragment []byte
|
||||
// The SQL WINDOW clause ORDER BY fragment(DEFAULT=[]byte("ORDER BY "))
|
||||
WindowOrderByFragment []byte
|
||||
// The SQL WINDOW clause OVER fragment(DEFAULT=[]byte(" OVER "))
|
||||
WindowOverFragment []byte
|
||||
// The SQL ORDER BY clause fragment(DEFAULT=[]byte(" ORDER BY "))
|
||||
OrderByFragment []byte
|
||||
// The SQL FETCH fragment(DEFAULT=[]byte(" "))
|
||||
FetchFragment []byte
|
||||
// The SQL LIMIT BY clause fragment(DEFAULT=[]byte(" LIMIT "))
|
||||
LimitFragment []byte
|
||||
// The SQL OFFSET BY clause fragment(DEFAULT=[]byte(" OFFSET "))
|
||||
OffsetFragment []byte
|
||||
// The SQL FOR UPDATE fragment(DEFAULT=[]byte(" FOR UPDATE "))
|
||||
ForUpdateFragment []byte
|
||||
// The SQL FOR NO KEY UPDATE fragment(DEFAULT=[]byte(" FOR NO KEY UPDATE "))
|
||||
ForNoKeyUpdateFragment []byte
|
||||
// The SQL FOR SHARE fragment(DEFAULT=[]byte(" FOR SHARE "))
|
||||
ForShareFragment []byte
|
||||
// The SQL OF fragment(DEFAULT=[]byte("OF "))
|
||||
OfFragment []byte
|
||||
// The SQL FOR KEY SHARE fragment(DEFAULT=[]byte(" FOR KEY SHARE "))
|
||||
ForKeyShareFragment []byte
|
||||
// The SQL NOWAIT fragment(DEFAULT=[]byte("NOWAIT"))
|
||||
NowaitFragment []byte
|
||||
// The SQL SKIP LOCKED fragment(DEFAULT=[]byte("SKIP LOCKED"))
|
||||
SkipLockedFragment []byte
|
||||
// The SQL AS fragment when aliasing an Expression(DEFAULT=[]byte(" AS "))
|
||||
AsFragment []byte
|
||||
// The SQL LATERAL fragment used for LATERAL joins
|
||||
LateralFragment []byte
|
||||
// The quote rune to use when quoting identifiers(DEFAULT='"')
|
||||
QuoteRune rune
|
||||
// The NULL literal to use when interpolating nulls values (DEFAULT=[]byte("NULL"))
|
||||
Null []byte
|
||||
// The TRUE literal to use when interpolating bool true values (DEFAULT=[]byte("TRUE"))
|
||||
True []byte
|
||||
// The FALSE literal to use when interpolating bool false values (DEFAULT=[]byte("FALSE"))
|
||||
False []byte
|
||||
// The ASC fragment when specifying column order (DEFAULT=[]byte(" ASC"))
|
||||
AscFragment []byte
|
||||
// The DESC fragment when specifying column order (DEFAULT=[]byte(" DESC"))
|
||||
DescFragment []byte
|
||||
// The NULLS FIRST fragment when specifying column order (DEFAULT=[]byte(" NULLS FIRST"))
|
||||
NullsFirstFragment []byte
|
||||
// The NULLS LAST fragment when specifying column order (DEFAULT=[]byte(" NULLS LAST"))
|
||||
NullsLastFragment []byte
|
||||
// The AND keyword used when joining ExpressionLists (DEFAULT=[]byte(" AND "))
|
||||
AndFragment []byte
|
||||
// The OR keyword used when joining ExpressionLists (DEFAULT=[]byte(" OR "))
|
||||
OrFragment []byte
|
||||
// The UNION keyword used when creating compound statements (DEFAULT=[]byte(" UNION "))
|
||||
UnionFragment []byte
|
||||
// The UNION ALL keyword used when creating compound statements (DEFAULT=[]byte(" UNION ALL "))
|
||||
UnionAllFragment []byte
|
||||
// The INTERSECT keyword used when creating compound statements (DEFAULT=[]byte(" INTERSECT "))
|
||||
IntersectFragment []byte
|
||||
// The INTERSECT ALL keyword used when creating compound statements (DEFAULT=[]byte(" INTERSECT ALL "))
|
||||
IntersectAllFragment []byte
|
||||
// The CAST keyword to use when casting a value (DEFAULT=[]byte("CAST"))
|
||||
CastFragment []byte
|
||||
// The CASE keyword to use when when creating a CASE statement (DEFAULT=[]byte("CASE "))
|
||||
CaseFragment []byte
|
||||
// The WHEN keyword to use when when creating a CASE statement (DEFAULT=[]byte(" WHEN "))
|
||||
WhenFragment []byte
|
||||
// The THEN keyword to use when when creating a CASE statement (DEFAULT=[]byte(" THEN "))
|
||||
ThenFragment []byte
|
||||
// The ELSE keyword to use when when creating a CASE statement (DEFAULT=[]byte(" ELSE "))
|
||||
ElseFragment []byte
|
||||
// The End keyword to use when when creating a CASE statement (DEFAULT=[]byte(" END"))
|
||||
EndFragment []byte
|
||||
// The quote rune to use when quoting string literals (DEFAULT='\'')
|
||||
StringQuote rune
|
||||
// The operator to use when setting values in an update statement (DEFAULT='=')
|
||||
SetOperatorRune rune
|
||||
// The placeholder fragment to use when generating a non interpolated statement (DEFAULT=[]byte"?")
|
||||
PlaceHolderFragment []byte
|
||||
// Empty string (DEFAULT="")
|
||||
EmptyString string
|
||||
// Comma rune (DEFAULT=',')
|
||||
CommaRune rune
|
||||
// Space rune (DEFAULT=' ')
|
||||
SpaceRune rune
|
||||
// Left paren rune (DEFAULT='(')
|
||||
LeftParenRune rune
|
||||
// Right paren rune (DEFAULT=')')
|
||||
RightParenRune rune
|
||||
// Star rune (DEFAULT='*')
|
||||
StarRune rune
|
||||
// Period rune (DEFAULT='.')
|
||||
PeriodRune rune
|
||||
// Set to true to include positional argument numbers when creating a prepared statement (Default=false)
|
||||
IncludePlaceholderNum bool
|
||||
// The time format to use when serializing time.Time (DEFAULT=time.RFC3339Nano)
|
||||
TimeFormat string
|
||||
// A map used to look up BooleanOperations and their SQL equivalents
|
||||
// (Default= map[exp.BooleanOperation][]byte{
|
||||
// exp.EqOp: []byte("="),
|
||||
// exp.NeqOp: []byte("!="),
|
||||
// exp.GtOp: []byte(">"),
|
||||
// exp.GteOp: []byte(">="),
|
||||
// exp.LtOp: []byte("<"),
|
||||
// exp.LteOp: []byte("<="),
|
||||
// exp.InOp: []byte("IN"),
|
||||
// exp.NotInOp: []byte("NOT IN"),
|
||||
// exp.IsOp: []byte("IS"),
|
||||
// exp.IsNotOp: []byte("IS NOT"),
|
||||
// exp.LikeOp: []byte("LIKE"),
|
||||
// exp.NotLikeOp: []byte("NOT LIKE"),
|
||||
// exp.ILikeOp: []byte("ILIKE"),
|
||||
// exp.NotILikeOp: []byte("NOT ILIKE"),
|
||||
// exp.RegexpLikeOp: []byte("~"),
|
||||
// exp.RegexpNotLikeOp: []byte("!~"),
|
||||
// exp.RegexpILikeOp: []byte("~*"),
|
||||
// exp.RegexpNotILikeOp: []byte("!~*"),
|
||||
// })
|
||||
BooleanOperatorLookup map[exp.BooleanOperation][]byte
|
||||
// A map used to look up BitwiseOperations and their SQL equivalents
|
||||
// (Default=map[exp.BitwiseOperation][]byte{
|
||||
// exp.BitwiseInversionOp: []byte("~"),
|
||||
// exp.BitwiseOrOp: []byte("|"),
|
||||
// exp.BitwiseAndOp: []byte("&"),
|
||||
// exp.BitwiseXorOp: []byte("#"),
|
||||
// exp.BitwiseLeftShiftOp: []byte("<<"),
|
||||
// exp.BitwiseRightShiftOp: []byte(">>"),
|
||||
// }),
|
||||
BitwiseOperatorLookup map[exp.BitwiseOperation][]byte
|
||||
// A map used to look up RangeOperations and their SQL equivalents
|
||||
// (Default=map[exp.RangeOperation][]byte{
|
||||
// exp.BetweenOp: []byte("BETWEEN"),
|
||||
// exp.NotBetweenOp: []byte("NOT BETWEEN"),
|
||||
// })
|
||||
RangeOperatorLookup map[exp.RangeOperation][]byte
|
||||
// A map used to look up JoinTypes and their SQL equivalents
|
||||
// (Default= map[exp.JoinType][]byte{
|
||||
// exp.InnerJoinType: []byte(" INNER JOIN "),
|
||||
// exp.FullOuterJoinType: []byte(" FULL OUTER JOIN "),
|
||||
// exp.RightOuterJoinType: []byte(" RIGHT OUTER JOIN "),
|
||||
// exp.LeftOuterJoinType: []byte(" LEFT OUTER JOIN "),
|
||||
// exp.FullJoinType: []byte(" FULL JOIN "),
|
||||
// exp.RightJoinType: []byte(" RIGHT JOIN "),
|
||||
// exp.LeftJoinType: []byte(" LEFT JOIN "),
|
||||
// exp.NaturalJoinType: []byte(" NATURAL JOIN "),
|
||||
// exp.NaturalLeftJoinType: []byte(" NATURAL LEFT JOIN "),
|
||||
// exp.NaturalRightJoinType: []byte(" NATURAL RIGHT JOIN "),
|
||||
// exp.NaturalFullJoinType: []byte(" NATURAL FULL JOIN "),
|
||||
// exp.CrossJoinType: []byte(" CROSS JOIN "),
|
||||
// })
|
||||
JoinTypeLookup map[exp.JoinType][]byte
|
||||
// Whether or not boolean data type is supported
|
||||
BooleanDataTypeSupported bool
|
||||
// Whether or not to use literal TRUE or FALSE for IS statements (e.g. IS TRUE or IS 0)
|
||||
UseLiteralIsBools bool
|
||||
// EscapedRunes is a map of a rune and the corresponding escape sequence in bytes. Used when escaping text
|
||||
// types.
|
||||
// (Default= map[rune][]byte{
|
||||
// '\'': []byte("''"),
|
||||
// })
|
||||
EscapedRunes map[rune][]byte
|
||||
|
||||
// The SQL fragment to use for CONFLICT (Default=[]byte(" ON CONFLICT"))
|
||||
ConflictFragment []byte
|
||||
// The SQL fragment to use for CONFLICT DO NOTHING (Default=[]byte(" DO NOTHING"))
|
||||
ConflictDoNothingFragment []byte
|
||||
// The SQL fragment to use for CONFLICT DO UPDATE (Default=[]byte(" DO UPDATE SET"))
|
||||
ConflictDoUpdateFragment []byte
|
||||
|
||||
// The order of SQL fragments when creating a SELECT statement
|
||||
// (Default=[]SQLFragmentType{
|
||||
// CommonTableSQLFragment,
|
||||
// SelectSQLFragment,
|
||||
// FromSQLFragment,
|
||||
// JoinSQLFragment,
|
||||
// WhereSQLFragment,
|
||||
// GroupBySQLFragment,
|
||||
// HavingSQLFragment,
|
||||
// CompoundsSQLFragment,
|
||||
// OrderSQLFragment,
|
||||
// LimitSQLFragment,
|
||||
// OffsetSQLFragment,
|
||||
// ForSQLFragment,
|
||||
// })
|
||||
SelectSQLOrder []SQLFragmentType
|
||||
|
||||
// The order of SQL fragments when creating an UPDATE statement
|
||||
// (Default=[]SQLFragmentType{
|
||||
// CommonTableSQLFragment,
|
||||
// UpdateBeginSQLFragment,
|
||||
// SourcesSQLFragment,
|
||||
// UpdateSQLFragment,
|
||||
// WhereSQLFragment,
|
||||
// OrderSQLFragment,
|
||||
// LimitSQLFragment,
|
||||
// ReturningSQLFragment,
|
||||
// })
|
||||
UpdateSQLOrder []SQLFragmentType
|
||||
|
||||
// The order of SQL fragments when creating an INSERT statement
|
||||
// (Default=[]SQLFragmentType{
|
||||
// CommonTableSQLFragment,
|
||||
// InsertBeingSQLFragment,
|
||||
// SourcesSQLFragment,
|
||||
// InsertSQLFragment,
|
||||
// ReturningSQLFragment,
|
||||
// })
|
||||
InsertSQLOrder []SQLFragmentType
|
||||
|
||||
// The order of SQL fragments when creating a DELETE statement
|
||||
// (Default=[]SQLFragmentType{
|
||||
// CommonTableSQLFragment,
|
||||
// DeleteBeginSQLFragment,
|
||||
// FromSQLFragment,
|
||||
// WhereSQLFragment,
|
||||
// OrderSQLFragment,
|
||||
// LimitSQLFragment,
|
||||
// ReturningSQLFragment,
|
||||
// })
|
||||
DeleteSQLOrder []SQLFragmentType
|
||||
|
||||
// The order of SQL fragments when creating a TRUNCATE statement
|
||||
// (Default=[]SQLFragmentType{
|
||||
// TruncateSQLFragment,
|
||||
// })
|
||||
TruncateSQLOrder []SQLFragmentType
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
CommonTableSQLFragment = iota
|
||||
SelectSQLFragment
|
||||
SelectWithLimitSQLFragment
|
||||
FromSQLFragment
|
||||
JoinSQLFragment
|
||||
WhereSQLFragment
|
||||
GroupBySQLFragment
|
||||
HavingSQLFragment
|
||||
CompoundsSQLFragment
|
||||
OrderSQLFragment
|
||||
OrderWithOffsetFetchSQLFragment
|
||||
LimitSQLFragment
|
||||
OffsetSQLFragment
|
||||
ForSQLFragment
|
||||
UpdateBeginSQLFragment
|
||||
SourcesSQLFragment
|
||||
IntoSQLFragment
|
||||
UpdateSQLFragment
|
||||
UpdateFromSQLFragment
|
||||
ReturningSQLFragment
|
||||
InsertBeingSQLFragment
|
||||
InsertSQLFragment
|
||||
DeleteBeginSQLFragment
|
||||
TruncateSQLFragment
|
||||
WindowSQLFragment
|
||||
)
|
||||
|
||||
//nolint:gocyclo // simple type to string conversion
|
||||
func (sf SQLFragmentType) String() string {
|
||||
switch sf {
|
||||
case CommonTableSQLFragment:
|
||||
return "CommonTableSQLFragment"
|
||||
case SelectSQLFragment:
|
||||
return "SelectSQLFragment"
|
||||
case FromSQLFragment:
|
||||
return "FromSQLFragment"
|
||||
case JoinSQLFragment:
|
||||
return "JoinSQLFragment"
|
||||
case WhereSQLFragment:
|
||||
return "WhereSQLFragment"
|
||||
case GroupBySQLFragment:
|
||||
return "GroupBySQLFragment"
|
||||
case HavingSQLFragment:
|
||||
return "HavingSQLFragment"
|
||||
case CompoundsSQLFragment:
|
||||
return "CompoundsSQLFragment"
|
||||
case OrderSQLFragment:
|
||||
return "OrderSQLFragment"
|
||||
case LimitSQLFragment:
|
||||
return "LimitSQLFragment"
|
||||
case OffsetSQLFragment:
|
||||
return "OffsetSQLFragment"
|
||||
case ForSQLFragment:
|
||||
return "ForSQLFragment"
|
||||
case UpdateBeginSQLFragment:
|
||||
return "UpdateBeginSQLFragment"
|
||||
case SourcesSQLFragment:
|
||||
return "SourcesSQLFragment"
|
||||
case IntoSQLFragment:
|
||||
return "IntoSQLFragment"
|
||||
case UpdateSQLFragment:
|
||||
return "UpdateSQLFragment"
|
||||
case UpdateFromSQLFragment:
|
||||
return "UpdateFromSQLFragment"
|
||||
case ReturningSQLFragment:
|
||||
return "ReturningSQLFragment"
|
||||
case InsertBeingSQLFragment:
|
||||
return "InsertBeingSQLFragment"
|
||||
case DeleteBeginSQLFragment:
|
||||
return "DeleteBeginSQLFragment"
|
||||
case TruncateSQLFragment:
|
||||
return "TruncateSQLFragment"
|
||||
case WindowSQLFragment:
|
||||
return "WindowSQLFragment"
|
||||
}
|
||||
return fmt.Sprintf("%d", sf)
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func DefaultDialectOptions() *SQLDialectOptions {
|
||||
return &SQLDialectOptions{
|
||||
SupportsOrderByOnDelete: false,
|
||||
SupportsDeleteTableHint: false,
|
||||
SupportsOrderByOnUpdate: false,
|
||||
SupportsLimitOnDelete: false,
|
||||
SupportsLimitOnUpdate: false,
|
||||
SupportsReturn: true,
|
||||
SupportsConflictUpdateWhere: true,
|
||||
SupportsInsertIgnoreSyntax: false,
|
||||
SupportsConflictTarget: true,
|
||||
SupportsWithCTE: true,
|
||||
SupportsWithCTERecursive: true,
|
||||
SupportsDistinctOn: true,
|
||||
WrapCompoundsInParens: true,
|
||||
SupportsWindowFunction: true,
|
||||
SupportsLateral: true,
|
||||
|
||||
SupportsMultipleUpdateTables: true,
|
||||
UseFromClauseForMultipleUpdateTables: true,
|
||||
|
||||
UpdateClause: []byte("UPDATE"),
|
||||
InsertClause: []byte("INSERT INTO"),
|
||||
InsertIgnoreClause: []byte("INSERT IGNORE INTO"),
|
||||
SelectClause: []byte("SELECT"),
|
||||
DeleteClause: []byte("DELETE"),
|
||||
TruncateClause: []byte("TRUNCATE"),
|
||||
WithFragment: []byte("WITH "),
|
||||
RecursiveFragment: []byte("RECURSIVE "),
|
||||
CascadeFragment: []byte(" CASCADE"),
|
||||
RestrictFragment: []byte(" RESTRICT"),
|
||||
DefaultValuesFragment: []byte(" DEFAULT VALUES"),
|
||||
ValuesFragment: []byte(" VALUES "),
|
||||
IdentityFragment: []byte(" IDENTITY"),
|
||||
SetFragment: []byte(" SET "),
|
||||
DistinctFragment: []byte("DISTINCT"),
|
||||
ReturningFragment: []byte(" RETURNING "),
|
||||
FromFragment: []byte(" FROM"),
|
||||
UsingFragment: []byte(" USING "),
|
||||
OnFragment: []byte(" ON "),
|
||||
WhereFragment: []byte(" WHERE "),
|
||||
GroupByFragment: []byte(" GROUP BY "),
|
||||
HavingFragment: []byte(" HAVING "),
|
||||
WindowFragment: []byte(" WINDOW "),
|
||||
WindowPartitionByFragment: []byte("PARTITION BY "),
|
||||
WindowOrderByFragment: []byte("ORDER BY "),
|
||||
WindowOverFragment: []byte(" OVER "),
|
||||
OrderByFragment: []byte(" ORDER BY "),
|
||||
FetchFragment: []byte(" "),
|
||||
LimitFragment: []byte(" LIMIT "),
|
||||
OffsetFragment: []byte(" OFFSET "),
|
||||
ForUpdateFragment: []byte(" FOR UPDATE "),
|
||||
ForNoKeyUpdateFragment: []byte(" FOR NO KEY UPDATE "),
|
||||
ForShareFragment: []byte(" FOR SHARE "),
|
||||
ForKeyShareFragment: []byte(" FOR KEY SHARE "),
|
||||
OfFragment: []byte("OF "),
|
||||
NowaitFragment: []byte("NOWAIT"),
|
||||
SkipLockedFragment: []byte("SKIP LOCKED"),
|
||||
LateralFragment: []byte("LATERAL "),
|
||||
AsFragment: []byte(" AS "),
|
||||
AscFragment: []byte(" ASC"),
|
||||
DescFragment: []byte(" DESC"),
|
||||
NullsFirstFragment: []byte(" NULLS FIRST"),
|
||||
NullsLastFragment: []byte(" NULLS LAST"),
|
||||
AndFragment: []byte(" AND "),
|
||||
OrFragment: []byte(" OR "),
|
||||
UnionFragment: []byte(" UNION "),
|
||||
UnionAllFragment: []byte(" UNION ALL "),
|
||||
IntersectFragment: []byte(" INTERSECT "),
|
||||
IntersectAllFragment: []byte(" INTERSECT ALL "),
|
||||
ConflictFragment: []byte(" ON CONFLICT"),
|
||||
ConflictDoUpdateFragment: []byte(" DO UPDATE SET "),
|
||||
ConflictDoNothingFragment: []byte(" DO NOTHING"),
|
||||
CastFragment: []byte("CAST"),
|
||||
CaseFragment: []byte("CASE "),
|
||||
WhenFragment: []byte(" WHEN "),
|
||||
ThenFragment: []byte(" THEN "),
|
||||
ElseFragment: []byte(" ELSE "),
|
||||
EndFragment: []byte(" END"),
|
||||
Null: []byte("NULL"),
|
||||
True: []byte("TRUE"),
|
||||
False: []byte("FALSE"),
|
||||
|
||||
PlaceHolderFragment: []byte("?"),
|
||||
QuoteRune: '"',
|
||||
StringQuote: '\'',
|
||||
SetOperatorRune: '=',
|
||||
CommaRune: ',',
|
||||
SpaceRune: ' ',
|
||||
LeftParenRune: '(',
|
||||
RightParenRune: ')',
|
||||
StarRune: '*',
|
||||
PeriodRune: '.',
|
||||
EmptyString: "",
|
||||
|
||||
BooleanOperatorLookup: map[exp.BooleanOperation][]byte{
|
||||
exp.EqOp: []byte("="),
|
||||
exp.NeqOp: []byte("!="),
|
||||
exp.GtOp: []byte(">"),
|
||||
exp.GteOp: []byte(">="),
|
||||
exp.LtOp: []byte("<"),
|
||||
exp.LteOp: []byte("<="),
|
||||
exp.InOp: []byte("IN"),
|
||||
exp.NotInOp: []byte("NOT IN"),
|
||||
exp.IsOp: []byte("IS"),
|
||||
exp.IsNotOp: []byte("IS NOT"),
|
||||
exp.LikeOp: []byte("LIKE"),
|
||||
exp.NotLikeOp: []byte("NOT LIKE"),
|
||||
exp.ILikeOp: []byte("ILIKE"),
|
||||
exp.NotILikeOp: []byte("NOT ILIKE"),
|
||||
exp.RegexpLikeOp: []byte("~"),
|
||||
exp.RegexpNotLikeOp: []byte("!~"),
|
||||
exp.RegexpILikeOp: []byte("~*"),
|
||||
exp.RegexpNotILikeOp: []byte("!~*"),
|
||||
},
|
||||
BitwiseOperatorLookup: map[exp.BitwiseOperation][]byte{
|
||||
exp.BitwiseInversionOp: []byte("~"),
|
||||
exp.BitwiseOrOp: []byte("|"),
|
||||
exp.BitwiseAndOp: []byte("&"),
|
||||
exp.BitwiseXorOp: []byte("#"),
|
||||
exp.BitwiseLeftShiftOp: []byte("<<"),
|
||||
exp.BitwiseRightShiftOp: []byte(">>"),
|
||||
},
|
||||
RangeOperatorLookup: map[exp.RangeOperation][]byte{
|
||||
exp.BetweenOp: []byte("BETWEEN"),
|
||||
exp.NotBetweenOp: []byte("NOT BETWEEN"),
|
||||
},
|
||||
JoinTypeLookup: map[exp.JoinType][]byte{
|
||||
exp.InnerJoinType: []byte(" INNER JOIN "),
|
||||
exp.FullOuterJoinType: []byte(" FULL OUTER JOIN "),
|
||||
exp.RightOuterJoinType: []byte(" RIGHT OUTER JOIN "),
|
||||
exp.LeftOuterJoinType: []byte(" LEFT OUTER JOIN "),
|
||||
exp.FullJoinType: []byte(" FULL JOIN "),
|
||||
exp.RightJoinType: []byte(" RIGHT JOIN "),
|
||||
exp.LeftJoinType: []byte(" LEFT JOIN "),
|
||||
exp.NaturalJoinType: []byte(" NATURAL JOIN "),
|
||||
exp.NaturalLeftJoinType: []byte(" NATURAL LEFT JOIN "),
|
||||
exp.NaturalRightJoinType: []byte(" NATURAL RIGHT JOIN "),
|
||||
exp.NaturalFullJoinType: []byte(" NATURAL FULL JOIN "),
|
||||
exp.CrossJoinType: []byte(" CROSS JOIN "),
|
||||
},
|
||||
|
||||
TimeFormat: time.RFC3339Nano,
|
||||
|
||||
BooleanDataTypeSupported: true,
|
||||
UseLiteralIsBools: true,
|
||||
|
||||
EscapedRunes: map[rune][]byte{
|
||||
'\'': []byte("''"),
|
||||
},
|
||||
|
||||
SelectSQLOrder: []SQLFragmentType{
|
||||
CommonTableSQLFragment,
|
||||
SelectSQLFragment,
|
||||
FromSQLFragment,
|
||||
JoinSQLFragment,
|
||||
WhereSQLFragment,
|
||||
GroupBySQLFragment,
|
||||
HavingSQLFragment,
|
||||
WindowSQLFragment,
|
||||
CompoundsSQLFragment,
|
||||
OrderSQLFragment,
|
||||
LimitSQLFragment,
|
||||
OffsetSQLFragment,
|
||||
ForSQLFragment,
|
||||
},
|
||||
UpdateSQLOrder: []SQLFragmentType{
|
||||
CommonTableSQLFragment,
|
||||
UpdateBeginSQLFragment,
|
||||
SourcesSQLFragment,
|
||||
UpdateSQLFragment,
|
||||
UpdateFromSQLFragment,
|
||||
WhereSQLFragment,
|
||||
OrderSQLFragment,
|
||||
LimitSQLFragment,
|
||||
ReturningSQLFragment,
|
||||
},
|
||||
InsertSQLOrder: []SQLFragmentType{
|
||||
CommonTableSQLFragment,
|
||||
InsertBeingSQLFragment,
|
||||
IntoSQLFragment,
|
||||
InsertSQLFragment,
|
||||
ReturningSQLFragment,
|
||||
},
|
||||
DeleteSQLOrder: []SQLFragmentType{
|
||||
CommonTableSQLFragment,
|
||||
DeleteBeginSQLFragment,
|
||||
FromSQLFragment,
|
||||
WhereSQLFragment,
|
||||
OrderSQLFragment,
|
||||
LimitSQLFragment,
|
||||
ReturningSQLFragment,
|
||||
},
|
||||
TruncateSQLOrder: []SQLFragmentType{
|
||||
TruncateSQLFragment,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type sqlFragmentTypeSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (sfts *sqlFragmentTypeSuite) TestOptions_SQLFragmentType() {
|
||||
for _, tt := range []struct {
|
||||
typ sqlgen.SQLFragmentType
|
||||
expectedStr string
|
||||
}{
|
||||
{typ: sqlgen.CommonTableSQLFragment, expectedStr: "CommonTableSQLFragment"},
|
||||
{typ: sqlgen.SelectSQLFragment, expectedStr: "SelectSQLFragment"},
|
||||
{typ: sqlgen.FromSQLFragment, expectedStr: "FromSQLFragment"},
|
||||
{typ: sqlgen.JoinSQLFragment, expectedStr: "JoinSQLFragment"},
|
||||
{typ: sqlgen.WhereSQLFragment, expectedStr: "WhereSQLFragment"},
|
||||
{typ: sqlgen.GroupBySQLFragment, expectedStr: "GroupBySQLFragment"},
|
||||
{typ: sqlgen.HavingSQLFragment, expectedStr: "HavingSQLFragment"},
|
||||
{typ: sqlgen.CompoundsSQLFragment, expectedStr: "CompoundsSQLFragment"},
|
||||
{typ: sqlgen.OrderSQLFragment, expectedStr: "OrderSQLFragment"},
|
||||
{typ: sqlgen.LimitSQLFragment, expectedStr: "LimitSQLFragment"},
|
||||
{typ: sqlgen.OffsetSQLFragment, expectedStr: "OffsetSQLFragment"},
|
||||
{typ: sqlgen.ForSQLFragment, expectedStr: "ForSQLFragment"},
|
||||
{typ: sqlgen.UpdateBeginSQLFragment, expectedStr: "UpdateBeginSQLFragment"},
|
||||
{typ: sqlgen.SourcesSQLFragment, expectedStr: "SourcesSQLFragment"},
|
||||
{typ: sqlgen.IntoSQLFragment, expectedStr: "IntoSQLFragment"},
|
||||
{typ: sqlgen.UpdateSQLFragment, expectedStr: "UpdateSQLFragment"},
|
||||
{typ: sqlgen.UpdateFromSQLFragment, expectedStr: "UpdateFromSQLFragment"},
|
||||
{typ: sqlgen.ReturningSQLFragment, expectedStr: "ReturningSQLFragment"},
|
||||
{typ: sqlgen.InsertBeingSQLFragment, expectedStr: "InsertBeingSQLFragment"},
|
||||
{typ: sqlgen.DeleteBeginSQLFragment, expectedStr: "DeleteBeginSQLFragment"},
|
||||
{typ: sqlgen.TruncateSQLFragment, expectedStr: "TruncateSQLFragment"},
|
||||
{typ: sqlgen.WindowSQLFragment, expectedStr: "WindowSQLFragment"},
|
||||
{typ: sqlgen.SQLFragmentType(10000), expectedStr: "10000"},
|
||||
} {
|
||||
sfts.Equal(tt.expectedStr, tt.typ.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLFragmentType(t *testing.T) {
|
||||
suite.Run(t, new(sqlFragmentTypeSuite))
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package sqlgen
|
||||
|
||||
import "time"
|
||||
|
||||
var timeLocation = time.UTC
|
||||
|
||||
// Set the location to use when interpolating time.Time instances. See https://golang.org/pkg/time/#LoadLocation
|
||||
// NOTE: This has no effect when using prepared statements.
|
||||
func SetTimeLocation(loc *time.Location) {
|
||||
timeLocation = loc
|
||||
}
|
||||
|
||||
func GetTimeLocation() *time.Location {
|
||||
return timeLocation
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
TruncateSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, clauses exp.TruncateClauses)
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
truncateSQLGenerator struct {
|
||||
CommonSQLGenerator
|
||||
}
|
||||
)
|
||||
|
||||
var errNoSourceForTruncate = errors.New("no source found when generating truncate sql")
|
||||
|
||||
func NewTruncateSQLGenerator(dialect string, do *SQLDialectOptions) TruncateSQLGenerator {
|
||||
return &truncateSQLGenerator{NewCommonSQLGenerator(dialect, do)}
|
||||
}
|
||||
|
||||
func (tsg *truncateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.TruncateClauses) {
|
||||
if !clauses.HasTable() {
|
||||
b.SetError(errNoSourceForTruncate)
|
||||
return
|
||||
}
|
||||
for _, f := range tsg.DialectOptions().TruncateSQLOrder {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
switch f {
|
||||
case TruncateSQLFragment:
|
||||
tsg.TruncateSQL(b, clauses.Table(), clauses.Options())
|
||||
default:
|
||||
b.SetError(ErrNotSupportedFragment("TRUNCATE", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a TRUNCATE statement
|
||||
func (tsg *truncateSQLGenerator) TruncateSQL(b sb.SQLBuilder, from exp.ColumnListExpression, opts exp.TruncateOptions) {
|
||||
b.Write(tsg.DialectOptions().TruncateClause)
|
||||
tsg.SourcesSQL(b, from)
|
||||
if opts.Identity != tsg.DialectOptions().EmptyString {
|
||||
b.WriteRunes(tsg.DialectOptions().SpaceRune).
|
||||
WriteStrings(strings.ToUpper(opts.Identity)).
|
||||
Write(tsg.DialectOptions().IdentityFragment)
|
||||
}
|
||||
if opts.Cascade {
|
||||
b.Write(tsg.DialectOptions().CascadeFragment)
|
||||
} else if opts.Restrict {
|
||||
b.Write(tsg.DialectOptions().RestrictFragment)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
truncateTestCase struct {
|
||||
clause exp.TruncateClauses
|
||||
sql string
|
||||
isPrepared bool
|
||||
args []interface{}
|
||||
err string
|
||||
}
|
||||
truncateSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) assertCases(tsg sqlgen.TruncateSQLGenerator, testCases ...truncateTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
tsg.Generate(b, tc.clause)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
tsgs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
tsgs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
tsgs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) TestDialect() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewTruncateSQLGenerator("test", opts)
|
||||
tsgs.Equal("test", d.Dialect())
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
d2 := sqlgen.NewTruncateSQLGenerator("test2", opts2)
|
||||
tsgs.Equal("test2", d2.Dialect())
|
||||
}
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) TestGenerate() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.TruncateClause = []byte("truncate")
|
||||
|
||||
tcNoTable := exp.NewTruncateClauses()
|
||||
tcSingle := tcNoTable.SetTable(exp.NewColumnListExpression("a"))
|
||||
tcMulti := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a", "b"))
|
||||
|
||||
expectedNoSourceErr := "db: no source found when generating truncate sql"
|
||||
tsgs.assertCases(
|
||||
sqlgen.NewTruncateSQLGenerator("test", opts),
|
||||
truncateTestCase{clause: tcSingle, sql: `truncate "a"`},
|
||||
truncateTestCase{clause: tcSingle, sql: `truncate "a"`, isPrepared: true},
|
||||
|
||||
truncateTestCase{clause: tcMulti, sql: `truncate "a", "b"`},
|
||||
truncateTestCase{clause: tcMulti, sql: `truncate "a", "b"`, isPrepared: true},
|
||||
|
||||
truncateTestCase{clause: tcNoTable, err: expectedNoSourceErr},
|
||||
truncateTestCase{clause: tcNoTable, err: expectedNoSourceErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) TestGenerate_UnsupportedFragment() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.TruncateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment}
|
||||
tc := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a"))
|
||||
expectedErr := "db: unsupported TRUNCATE SQL fragment UpdateBeginSQLFragment"
|
||||
tsgs.assertCases(
|
||||
sqlgen.NewTruncateSQLGenerator("test", opts),
|
||||
truncateTestCase{clause: tc, err: expectedErr},
|
||||
truncateTestCase{clause: tc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) TestGenerate_WithErroredBuilder() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.TruncateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.UpdateBeginSQLFragment}
|
||||
d := sqlgen.NewTruncateSQLGenerator("test", opts)
|
||||
|
||||
b := sb.NewSQLBuilder(true).SetError(errors.New("expected error"))
|
||||
d.Generate(b, exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a")))
|
||||
tsgs.assertErrorSQL(b, `db: expected error`)
|
||||
}
|
||||
|
||||
func (tsgs *truncateSQLGeneratorSuite) TestGenerate_WithCascade() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.CascadeFragment = []byte(" cascade")
|
||||
opts.RestrictFragment = []byte(" restrict")
|
||||
opts.IdentityFragment = []byte(" identity")
|
||||
|
||||
tc := exp.NewTruncateClauses().SetTable(exp.NewColumnListExpression("a"))
|
||||
tcCascade := tc.SetOptions(exp.TruncateOptions{Cascade: true})
|
||||
tcRestrict := tc.SetOptions(exp.TruncateOptions{Restrict: true})
|
||||
tcRestart := tc.SetOptions(exp.TruncateOptions{Identity: "restart"})
|
||||
|
||||
tsgs.assertCases(
|
||||
sqlgen.NewTruncateSQLGenerator("test", opts),
|
||||
truncateTestCase{clause: tcCascade, sql: `TRUNCATE "a" cascade`},
|
||||
truncateTestCase{clause: tcCascade, sql: `TRUNCATE "a" cascade`, isPrepared: true},
|
||||
|
||||
truncateTestCase{clause: tcRestrict, sql: `TRUNCATE "a" restrict`},
|
||||
truncateTestCase{clause: tcRestrict, sql: `TRUNCATE "a" restrict`, isPrepared: true},
|
||||
|
||||
truncateTestCase{clause: tcRestart, sql: `TRUNCATE "a" RESTART identity`},
|
||||
truncateTestCase{clause: tcRestart, sql: `TRUNCATE "a" RESTART identity`, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func TestTruncateSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(truncateSQLGeneratorSuite))
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package sqlgen
|
||||
|
||||
import (
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/errors"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
)
|
||||
|
||||
type (
|
||||
// An adapter interface to be used by a Dataset to generate SQL for a specific dialect.
|
||||
// See DefaultAdapter for a concrete implementation and examples.
|
||||
UpdateSQLGenerator interface {
|
||||
Dialect() string
|
||||
Generate(b sb.SQLBuilder, clauses exp.UpdateClauses)
|
||||
}
|
||||
// The default adapter. This class should be used when building a new adapter. When creating a new adapter you can
|
||||
// either override methods, or more typically update default values.
|
||||
// See (github.com/doug-martin/goqu/dialect/postgres)
|
||||
updateSQLGenerator struct {
|
||||
CommonSQLGenerator
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoSourceForUpdate = errors.New("no source found when generating update sql")
|
||||
ErrNoSetValuesForUpdate = errors.New("no set values found when generating UPDATE sql")
|
||||
)
|
||||
|
||||
func NewUpdateSQLGenerator(dialect string, do *SQLDialectOptions) UpdateSQLGenerator {
|
||||
return &updateSQLGenerator{NewCommonSQLGenerator(dialect, do)}
|
||||
}
|
||||
|
||||
func (usg *updateSQLGenerator) Generate(b sb.SQLBuilder, clauses exp.UpdateClauses) {
|
||||
if !clauses.HasTable() {
|
||||
b.SetError(ErrNoSourceForUpdate)
|
||||
return
|
||||
}
|
||||
if !clauses.HasSetValues() {
|
||||
b.SetError(ErrNoSetValuesForUpdate)
|
||||
return
|
||||
}
|
||||
if !usg.DialectOptions().SupportsMultipleUpdateTables && clauses.HasFrom() {
|
||||
b.SetError(errors.New("%s dialect does not support multiple tables in UPDATE", usg.Dialect()))
|
||||
}
|
||||
updates, err := exp.NewUpdateExpressions(clauses.SetValues())
|
||||
if err != nil {
|
||||
b.SetError(err)
|
||||
return
|
||||
}
|
||||
for _, f := range usg.DialectOptions().UpdateSQLOrder {
|
||||
if b.Error() != nil {
|
||||
return
|
||||
}
|
||||
switch f {
|
||||
case CommonTableSQLFragment:
|
||||
usg.ExpressionSQLGenerator().Generate(b, clauses.CommonTables())
|
||||
case UpdateBeginSQLFragment:
|
||||
usg.UpdateBeginSQL(b)
|
||||
case SourcesSQLFragment:
|
||||
usg.updateTableSQL(b, clauses)
|
||||
case UpdateSQLFragment:
|
||||
usg.UpdateExpressionsSQL(b, updates...)
|
||||
case UpdateFromSQLFragment:
|
||||
usg.updateFromSQL(b, clauses.From())
|
||||
case WhereSQLFragment:
|
||||
usg.WhereSQL(b, clauses.Where())
|
||||
case OrderSQLFragment:
|
||||
if usg.DialectOptions().SupportsOrderByOnUpdate {
|
||||
usg.OrderSQL(b, clauses.Order())
|
||||
}
|
||||
case LimitSQLFragment:
|
||||
if usg.DialectOptions().SupportsLimitOnUpdate {
|
||||
usg.LimitSQL(b, clauses.Limit())
|
||||
}
|
||||
case ReturningSQLFragment:
|
||||
usg.ReturningSQL(b, clauses.Returning())
|
||||
default:
|
||||
b.SetError(ErrNotSupportedFragment("UPDATE", f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the correct fragment to being an UPDATE statement
|
||||
func (usg *updateSQLGenerator) UpdateBeginSQL(b sb.SQLBuilder) {
|
||||
b.Write(usg.DialectOptions().UpdateClause)
|
||||
}
|
||||
|
||||
// Adds column setters in an update SET clause
|
||||
func (usg *updateSQLGenerator) UpdateExpressionsSQL(b sb.SQLBuilder, updates ...exp.UpdateExpression) {
|
||||
b.Write(usg.DialectOptions().SetFragment)
|
||||
usg.UpdateExpressionSQL(b, updates...)
|
||||
}
|
||||
|
||||
func (usg *updateSQLGenerator) updateTableSQL(b sb.SQLBuilder, uc exp.UpdateClauses) {
|
||||
b.WriteRunes(usg.DialectOptions().SpaceRune)
|
||||
usg.ExpressionSQLGenerator().Generate(b, uc.Table())
|
||||
if uc.HasFrom() {
|
||||
if !usg.DialectOptions().UseFromClauseForMultipleUpdateTables {
|
||||
b.WriteRunes(usg.DialectOptions().CommaRune)
|
||||
usg.ExpressionSQLGenerator().Generate(b, uc.From())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (usg *updateSQLGenerator) updateFromSQL(b sb.SQLBuilder, ce exp.ColumnListExpression) {
|
||||
if ce == nil || ce.IsEmpty() {
|
||||
return
|
||||
}
|
||||
if usg.DialectOptions().UseFromClauseForMultipleUpdateTables {
|
||||
usg.FromSQL(b, ce)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package sqlgen_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/db/v2/exp"
|
||||
"git.fsdpf.net/go/db/v2/internal/sb"
|
||||
"git.fsdpf.net/go/db/v2/sqlgen"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type (
|
||||
updateTestCase struct {
|
||||
clause exp.UpdateClauses
|
||||
sql string
|
||||
isPrepared bool
|
||||
args []interface{}
|
||||
err string
|
||||
}
|
||||
updateSQLGeneratorSuite struct {
|
||||
baseSQLGeneratorSuite
|
||||
}
|
||||
)
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) assertCases(usg sqlgen.UpdateSQLGenerator, testCases ...updateTestCase) {
|
||||
for _, tc := range testCases {
|
||||
b := sb.NewSQLBuilder(tc.isPrepared)
|
||||
usg.Generate(b, tc.clause)
|
||||
switch {
|
||||
case len(tc.err) > 0:
|
||||
usgs.assertErrorSQL(b, tc.err)
|
||||
case tc.isPrepared:
|
||||
usgs.assertPreparedSQL(b, tc.sql, tc.args)
|
||||
default:
|
||||
usgs.assertNotPreparedSQL(b, tc.sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestDialect() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
d := sqlgen.NewUpdateSQLGenerator("test", opts)
|
||||
usgs.Equal("test", d.Dialect())
|
||||
|
||||
opts2 := sqlgen.DefaultDialectOptions()
|
||||
d2 := sqlgen.NewUpdateSQLGenerator("test2", opts2)
|
||||
usgs.Equal("test2", d2.Dialect())
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_unsupportedFragment() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.UpdateSQLOrder = []sqlgen.SQLFragmentType{sqlgen.InsertBeingSQLFragment}
|
||||
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(exp.Record{"a": "b", "b": "c"})
|
||||
expectedErr := "db: unsupported UPDATE SQL fragment InsertBeingSQLFragment"
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, err: expectedErr},
|
||||
updateTestCase{clause: uc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_empty() {
|
||||
uc := exp.NewUpdateClauses()
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
updateTestCase{clause: uc, err: sqlgen.ErrNoSourceForUpdate.Error()},
|
||||
updateTestCase{clause: uc, err: sqlgen.ErrNoSourceForUpdate.Error(), isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withBadUpdateValues() {
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(true)
|
||||
|
||||
expectedErr := "db: unsupported update interface type bool"
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
updateTestCase{clause: uc, err: expectedErr},
|
||||
updateTestCase{clause: uc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_noSetValues() {
|
||||
uc := exp.NewUpdateClauses().SetTable(exp.NewIdentifierExpression("", "test", ""))
|
||||
|
||||
expectedErr := sqlgen.ErrNoSetValuesForUpdate.Error()
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
updateTestCase{clause: uc, err: expectedErr},
|
||||
updateTestCase{clause: uc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withFrom() {
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(exp.Record{"foo": "bar"}).
|
||||
SetFrom(exp.NewColumnListExpression("other_test"))
|
||||
|
||||
ucNullSet := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(exp.Record{"foo": nil}).
|
||||
SetFrom(exp.NewColumnListExpression("other_test"))
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "foo"='bar' FROM "other_test"`},
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "foo"=? FROM "other_test"`, isPrepared: true, args: []interface{}{"bar"}},
|
||||
|
||||
updateTestCase{clause: ucNullSet, sql: `UPDATE "test" SET "foo"=NULL FROM "other_test"`},
|
||||
updateTestCase{clause: ucNullSet, sql: `UPDATE "test" SET "foo"=? FROM "other_test"`, isPrepared: true, args: []interface{}{nil}},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.UseFromClauseForMultipleUpdateTables = false
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test","other_test" SET "foo"='bar'`},
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test","other_test" SET "foo"=?`, isPrepared: true, args: []interface{}{"bar"}},
|
||||
|
||||
updateTestCase{clause: ucNullSet, sql: `UPDATE "test","other_test" SET "foo"=NULL`},
|
||||
updateTestCase{clause: ucNullSet, sql: `UPDATE "test","other_test" SET "foo"=?`, isPrepared: true, args: []interface{}{nil}},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsMultipleUpdateTables = false
|
||||
expectedErr := "db: test dialect does not support multiple tables in UPDATE"
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, err: expectedErr},
|
||||
updateTestCase{clause: uc, err: expectedErr, isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withUpdateExpression() {
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
// make sure the fragments are used
|
||||
opts.SetFragment = []byte(" set ")
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", ""))
|
||||
ucRecord := uc.SetSetValues(exp.Record{"a": "b", "b": "c"})
|
||||
ucRecordNullVal := uc.SetSetValues(exp.Record{"a": "b", "b": nil})
|
||||
ucRecordBoolVals := uc.SetSetValues(exp.Record{"a": true, "b": false})
|
||||
ucEmptyRecord := uc.SetSetValues(exp.Record{})
|
||||
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: ucRecord, sql: `UPDATE "test" set "a"='b',"b"='c'`},
|
||||
updateTestCase{clause: ucRecord, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}},
|
||||
|
||||
updateTestCase{clause: ucRecordNullVal, sql: `UPDATE "test" set "a"='b',"b"=NULL`},
|
||||
updateTestCase{clause: ucRecordNullVal, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", nil}},
|
||||
|
||||
updateTestCase{clause: ucRecordBoolVals, sql: `UPDATE "test" set "a"=TRUE,"b"=FALSE`},
|
||||
updateTestCase{clause: ucRecordBoolVals, sql: `UPDATE "test" set "a"=?,"b"=?`, isPrepared: true, args: []interface{}{true, false}},
|
||||
|
||||
updateTestCase{clause: ucEmptyRecord, err: sqlgen.ErrNoUpdatedValuesProvided.Error()},
|
||||
updateTestCase{clause: ucEmptyRecord, err: sqlgen.ErrNoUpdatedValuesProvided.Error(), isPrepared: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withOrder() {
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(exp.Record{"a": "b", "b": "c"}).
|
||||
SetOrder(
|
||||
exp.NewIdentifierExpression("", "", "a").Asc(),
|
||||
exp.NewIdentifierExpression("", "", "b").Desc(),
|
||||
)
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsOrderByOnUpdate = true
|
||||
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c' ORDER BY "a" ASC, "b" DESC`},
|
||||
updateTestCase{
|
||||
clause: uc,
|
||||
sql: `UPDATE "test" SET "a"=?,"b"=? ORDER BY "a" ASC, "b" DESC`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsOrderByOnUpdate = false
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c'`},
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withLimit() {
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test", "")).
|
||||
SetSetValues(exp.Record{"a": "b", "b": "c"}).
|
||||
SetLimit(10)
|
||||
|
||||
opts := sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsLimitOnUpdate = true
|
||||
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c' LIMIT 10`},
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=? LIMIT ?`, isPrepared: true, args: []interface{}{"b", "c", int64(10)}},
|
||||
)
|
||||
|
||||
opts = sqlgen.DefaultDialectOptions()
|
||||
opts.SupportsLimitOnUpdate = false
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", opts),
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"='b',"b"='c'`},
|
||||
updateTestCase{clause: uc, sql: `UPDATE "test" SET "a"=?,"b"=?`, isPrepared: true, args: []interface{}{"b", "c"}},
|
||||
)
|
||||
}
|
||||
|
||||
func (usgs *updateSQLGeneratorSuite) TestGenerate_withCommonTables() {
|
||||
tse := newTestAppendableExpression("select * from foo", emptyArgs, nil, nil)
|
||||
uc := exp.NewUpdateClauses().
|
||||
SetTable(exp.NewIdentifierExpression("", "test_cte", "")).
|
||||
SetSetValues(exp.Record{"a": "b", "b": "c"})
|
||||
ucCte1 := uc.CommonTablesAppend(exp.NewCommonTableExpression(false, "test_cte", tse))
|
||||
ucCte2 := uc.CommonTablesAppend(exp.NewCommonTableExpression(true, "test_cte", tse))
|
||||
|
||||
usgs.assertCases(
|
||||
sqlgen.NewUpdateSQLGenerator("test", sqlgen.DefaultDialectOptions()),
|
||||
updateTestCase{
|
||||
clause: ucCte1,
|
||||
sql: `WITH test_cte AS (select * from foo) UPDATE "test_cte" SET "a"='b',"b"='c'`,
|
||||
},
|
||||
updateTestCase{
|
||||
clause: ucCte1,
|
||||
sql: `WITH test_cte AS (select * from foo) UPDATE "test_cte" SET "a"=?,"b"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
|
||||
updateTestCase{
|
||||
clause: ucCte2,
|
||||
sql: `WITH RECURSIVE test_cte AS (select * from foo) UPDATE "test_cte" SET "a"='b',"b"='c'`,
|
||||
},
|
||||
updateTestCase{
|
||||
clause: ucCte2,
|
||||
sql: `WITH RECURSIVE test_cte AS (select * from foo) UPDATE "test_cte" SET "a"=?,"b"=?`,
|
||||
isPrepared: true,
|
||||
args: []interface{}{"b", "c"},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestUpdateSQLGenerator(t *testing.T) {
|
||||
suite.Run(t, new(updateSQLGeneratorSuite))
|
||||
}
|
||||
Reference in New Issue
Block a user