fork github.com/doug-martin

This commit is contained in:
2025-03-22 23:02:05 +08:00
commit f14642a736
131 changed files with 34555 additions and 0 deletions
+39
View File
@@ -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)
}
+155
View File
@@ -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)
}
}
}
+339
View File
@@ -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))
}
+72
View File
@@ -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)
}
}
+233
View File
@@ -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))
}
+736
View File
@@ -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
+203
View File
@@ -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())
}
}
+468
View File
@@ -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))
}
+31
View File
@@ -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)
}
+31
View File
@@ -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)
}
+31
View File
@@ -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)
}
+31
View File
@@ -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)
}
+31
View File
@@ -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)
}
+266
View File
@@ -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())
}
+615
View File
@@ -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))
}
+607
View File
@@ -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,
},
}
}
+49
View File
@@ -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))
}
+15
View File
@@ -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
}
+64
View File
@@ -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)
}
}
+121
View File
@@ -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))
}
+112
View File
@@ -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)
}
}
+259
View File
@@ -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))
}