Skip to content

Commit

Permalink
feat(mysql): support ORDER BY and LIMIT clauses in UPDATE and DELETE …
Browse files Browse the repository at this point in the history
…statements
  • Loading branch information
capcom6 committed Nov 7, 2024
1 parent e51aa5b commit de71bed
Show file tree
Hide file tree
Showing 35 changed files with 266 additions and 89 deletions.
4 changes: 3 additions & 1 deletion dialect/feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,7 @@ const (
UpdateFromTable
MSSavepoint
GeneratedIdentity
CompositeIn // ... WHERE (A,B) IN ((N, NN), (N, NN)...)
CompositeIn // ... WHERE (A,B) IN ((N, NN), (N, NN)...)
UpdateOrderLimit // UPDATE ... ORDER BY ... LIMIT ...
DeleteOrderLimit // DELETE ... ORDER BY ... LIMIT ...
)
4 changes: 3 additions & 1 deletion dialect/mysqldialect/dialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ func New(opts ...DialectOption) *Dialect {
feature.InsertIgnore |
feature.InsertOnDuplicateKey |
feature.SelectExists |
feature.CompositeIn
feature.CompositeIn |
feature.UpdateOrderLimit |
feature.DeleteOrderLimit

for _, opt := range opts {
opt(d)
Expand Down
29 changes: 29 additions & 0 deletions internal/dbtest/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,35 @@ func TestQuery(t *testing.T) {
return db.NewInsert().Model(new(Model))
},
},
{
id: 168,
query: func(db *bun.DB) schema.QueryAppender {
// DELETE ... ORDER BY ... (MySQL, MariaDB)
return db.NewDelete().Model(new(Model)).WherePK().Order("id")
},
},
{
id: 169,
query: func(db *bun.DB) schema.QueryAppender {
// DELETE ... ORDER BY ... LIMIT ... (MySQL, MariaDB)
return db.NewDelete().Model(new(Model)).WherePK().Order("id").Limit(1)
},
},
{
id: 170,
query: func(db *bun.DB) schema.QueryAppender {
// DELETE ... USING ... ORDER BY ... LIMIT ... (MySQL, MariaDB)
return db.NewDelete().Model(new(Story)).TableExpr("archived_stories AS src").
Where("src.id = story.id").Order("src.id").Limit(1)
},
},
{
id: 171,
query: func(db *bun.DB) schema.QueryAppender {
// UPDATE ... SET ... ORDER BY ... LIMIT ... (MySQL, MariaDB)
return db.NewUpdate().Model(new(Story)).Set("name = ?", "new-name").WherePK().Order("id").Limit(1)
},
},
}

timeRE := regexp.MustCompile(`'2\d{3}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?(\+\d{2}:\d{2})?'`)
Expand Down
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mariadb-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id`
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mariadb-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mariadb-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: can't use ORDER or LIMIT with multiple tables
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mariadb-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mssql2019-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: order is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mssql2019-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mssql2019-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mssql2019-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql5-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id`
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql5-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql5-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: can't use ORDER or LIMIT with multiple tables
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql5-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql8-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` AS `model` WHERE (`model`.`id` = NULL) ORDER BY `id`
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql8-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM `models` AS `model` WHERE (`model`.`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql8-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: can't use ORDER or LIMIT with multiple tables
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-mysql8-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pg-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: order is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pg-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pg-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pg-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pgx-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: order is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pgx-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pgx-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-pgx-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-sqlite-168
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: order is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-sqlite-169
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-sqlite-170
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
1 change: 1 addition & 0 deletions internal/dbtest/testdata/snapshots/TestQuery-sqlite-171
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bun: limit is not supported for current dialect
112 changes: 112 additions & 0 deletions query_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/uptrace/bun/dialect"
Expand Down Expand Up @@ -1352,3 +1354,113 @@ func (ih *idxHintsQuery) bufIndexHint(
b = append(b, ")"...)
return b, nil
}

//------------------------------------------------------------------------------

type orderLimitOffsetQuery struct {
order []schema.QueryWithArgs

limit int32
offset int32
}

func (q *orderLimitOffsetQuery) addOrder(orders ...string) {
for _, order := range orders {
if order == "" {
continue
}

index := strings.IndexByte(order, ' ')
if index == -1 {
q.order = append(q.order, schema.UnsafeIdent(order))
continue
}

field := order[:index]
sort := order[index+1:]

switch strings.ToUpper(sort) {
case "ASC", "DESC", "ASC NULLS FIRST", "DESC NULLS FIRST",
"ASC NULLS LAST", "DESC NULLS LAST":
q.order = append(q.order, schema.SafeQuery("? ?", []interface{}{
Ident(field),
Safe(sort),
}))
default:
q.order = append(q.order, schema.UnsafeIdent(order))
}
}

}

func (q *orderLimitOffsetQuery) addOrderExpr(query string, args ...interface{}) {
q.order = append(q.order, schema.SafeQuery(query, args))
}

func (q *orderLimitOffsetQuery) appendOrder(fmter schema.Formatter, b []byte) (_ []byte, err error) {
if len(q.order) > 0 {
b = append(b, " ORDER BY "...)

for i, f := range q.order {
if i > 0 {
b = append(b, ", "...)
}
b, err = f.AppendQuery(fmter, b)
if err != nil {
return nil, err
}
}

return b, nil
}

// MSSQL: allows Limit() without Order() as per https://stackoverflow.com/a/36156953
if q.limit > 0 && fmter.Dialect().Name() == dialect.MSSQL {
return append(b, " ORDER BY _temp_sort"...), nil
}

return b, nil
}

func (q *orderLimitOffsetQuery) setLimit(n int) {
q.limit = int32(n)
}

func (q *orderLimitOffsetQuery) setOffset(n int) {
q.offset = int32(n)
}

func (q *orderLimitOffsetQuery) appendLimitOffset(fmter schema.Formatter, b []byte) (_ []byte, err error) {
if fmter.Dialect().Features().Has(feature.OffsetFetch) {
if q.limit > 0 && q.offset > 0 {
b = append(b, " OFFSET "...)
b = strconv.AppendInt(b, int64(q.offset), 10)
b = append(b, " ROWS"...)

b = append(b, " FETCH NEXT "...)
b = strconv.AppendInt(b, int64(q.limit), 10)
b = append(b, " ROWS ONLY"...)
} else if q.limit > 0 {
b = append(b, " OFFSET 0 ROWS"...)

b = append(b, " FETCH NEXT "...)
b = strconv.AppendInt(b, int64(q.limit), 10)
b = append(b, " ROWS ONLY"...)
} else if q.offset > 0 {
b = append(b, " OFFSET "...)
b = strconv.AppendInt(b, int64(q.offset), 10)
b = append(b, " ROWS"...)
}
} else {
if q.limit > 0 {
b = append(b, " LIMIT "...)
b = strconv.AppendInt(b, int64(q.limit), 10)
}
if q.offset > 0 {
b = append(b, " OFFSET "...)
b = strconv.AppendInt(b, int64(q.offset), 10)
}
}

return b, nil
}
44 changes: 44 additions & 0 deletions query_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bun
import (
"context"
"database/sql"
"errors"
"time"

"github.com/uptrace/bun/dialect/feature"
Expand All @@ -12,6 +13,7 @@ import (

type DeleteQuery struct {
whereBaseQuery
orderLimitOffsetQuery
returningQuery
}

Expand Down Expand Up @@ -120,11 +122,39 @@ func (q *DeleteQuery) WhereAllWithDeleted() *DeleteQuery {
return q
}

func (q *DeleteQuery) Order(orders ...string) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.err = errors.New("bun: order is not supported for current dialect")
return q
}
q.addOrder(orders...)
return q
}

func (q *DeleteQuery) OrderExpr(query string, args ...interface{}) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.err = errors.New("bun: order is not supported for current dialect")
return q
}
q.addOrderExpr(query, args...)
return q
}

func (q *DeleteQuery) ForceDelete() *DeleteQuery {
q.flags = q.flags.Set(forceDeleteFlag)
return q
}

// ------------------------------------------------------------------------------
func (q *DeleteQuery) Limit(n int) *DeleteQuery {
if !q.hasFeature(feature.DeleteOrderLimit) {
q.err = errors.New("bun: limit is not supported for current dialect")
return q
}
q.setLimit(n)
return q
}

//------------------------------------------------------------------------------

// Returning adds a RETURNING clause to the query.
Expand Down Expand Up @@ -203,6 +233,20 @@ func (q *DeleteQuery) AppendQuery(fmter schema.Formatter, b []byte) (_ []byte, e
return nil, err
}

if q.hasMultiTables() && (len(q.order) > 0 || q.limit > 0) {
return nil, errors.New("bun: can't use ORDER or LIMIT with multiple tables")
}

b, err = q.appendOrder(fmter, b)
if err != nil {
return nil, err
}

b, err = q.appendLimitOffset(fmter, b)
if err != nil {
return nil, err
}

if q.hasFeature(feature.Returning) && q.hasReturning() {
b = append(b, " RETURNING "...)
b, err = q.appendReturning(fmter, b)
Expand Down
Loading

0 comments on commit de71bed

Please sign in to comment.