From de71bed9252980648269af85b7a51cbc464ce710 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Sat, 2 Nov 2024 12:19:35 +0700 Subject: [PATCH] feat(mysql): support ORDER BY and LIMIT clauses in UPDATE and DELETE statements --- dialect/feature/feature.go | 4 +- dialect/mysqldialect/dialect.go | 4 +- internal/dbtest/query_test.go | 29 +++++ .../testdata/snapshots/TestQuery-mariadb-168 | 1 + .../testdata/snapshots/TestQuery-mariadb-169 | 1 + .../testdata/snapshots/TestQuery-mariadb-170 | 1 + .../testdata/snapshots/TestQuery-mariadb-171 | 1 + .../snapshots/TestQuery-mssql2019-168 | 1 + .../snapshots/TestQuery-mssql2019-169 | 1 + .../snapshots/TestQuery-mssql2019-170 | 1 + .../snapshots/TestQuery-mssql2019-171 | 1 + .../testdata/snapshots/TestQuery-mysql5-168 | 1 + .../testdata/snapshots/TestQuery-mysql5-169 | 1 + .../testdata/snapshots/TestQuery-mysql5-170 | 1 + .../testdata/snapshots/TestQuery-mysql5-171 | 1 + .../testdata/snapshots/TestQuery-mysql8-168 | 1 + .../testdata/snapshots/TestQuery-mysql8-169 | 1 + .../testdata/snapshots/TestQuery-mysql8-170 | 1 + .../testdata/snapshots/TestQuery-mysql8-171 | 1 + .../testdata/snapshots/TestQuery-pg-168 | 1 + .../testdata/snapshots/TestQuery-pg-169 | 1 + .../testdata/snapshots/TestQuery-pg-170 | 1 + .../testdata/snapshots/TestQuery-pg-171 | 1 + .../testdata/snapshots/TestQuery-pgx-168 | 1 + .../testdata/snapshots/TestQuery-pgx-169 | 1 + .../testdata/snapshots/TestQuery-pgx-170 | 1 + .../testdata/snapshots/TestQuery-pgx-171 | 1 + .../testdata/snapshots/TestQuery-sqlite-168 | 1 + .../testdata/snapshots/TestQuery-sqlite-169 | 1 + .../testdata/snapshots/TestQuery-sqlite-170 | 1 + .../testdata/snapshots/TestQuery-sqlite-171 | 1 + query_base.go | 112 ++++++++++++++++++ query_delete.go | 44 +++++++ query_select.go | 95 ++------------- query_update.go | 39 ++++++ 35 files changed, 266 insertions(+), 89 deletions(-) create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mariadb-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mariadb-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mariadb-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mariadb-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mssql2019-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mssql2019-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mssql2019-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mssql2019-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql5-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql5-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql5-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql5-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql8-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql8-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql8-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-mysql8-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pg-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pg-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pg-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pg-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pgx-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pgx-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pgx-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-pgx-171 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-sqlite-168 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-sqlite-169 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-sqlite-170 create mode 100644 internal/dbtest/testdata/snapshots/TestQuery-sqlite-171 diff --git a/dialect/feature/feature.go b/dialect/feature/feature.go index e311394d5..536561b5d 100644 --- a/dialect/feature/feature.go +++ b/dialect/feature/feature.go @@ -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 ... ) diff --git a/dialect/mysqldialect/dialect.go b/dialect/mysqldialect/dialect.go index 881aa7ebf..133293a36 100644 --- a/dialect/mysqldialect/dialect.go +++ b/dialect/mysqldialect/dialect.go @@ -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) diff --git a/internal/dbtest/query_test.go b/internal/dbtest/query_test.go index f232d77e2..045fe2a13 100644 --- a/internal/dbtest/query_test.go +++ b/internal/dbtest/query_test.go @@ -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})?'`) diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mariadb-168 b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-168 new file mode 100644 index 000000000..037d5eda3 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-168 @@ -0,0 +1 @@ +DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mariadb-169 b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-169 new file mode 100644 index 000000000..5c2babd09 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-169 @@ -0,0 +1 @@ +DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mariadb-170 b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-170 new file mode 100644 index 000000000..7d198652f --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-170 @@ -0,0 +1 @@ +bun: can't use ORDER or LIMIT with multiple tables diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mariadb-171 b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-171 new file mode 100644 index 000000000..887c9a7df --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mariadb-171 @@ -0,0 +1 @@ +UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-168 b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-168 new file mode 100644 index 000000000..99ac97add --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-168 @@ -0,0 +1 @@ +bun: order is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-169 b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-169 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-169 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-170 b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-170 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-170 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-171 b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-171 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mssql2019-171 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql5-168 b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-168 new file mode 100644 index 000000000..037d5eda3 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-168 @@ -0,0 +1 @@ +DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql5-169 b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-169 new file mode 100644 index 000000000..5c2babd09 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-169 @@ -0,0 +1 @@ +DELETE FROM `models` WHERE (`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql5-170 b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-170 new file mode 100644 index 000000000..7d198652f --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-170 @@ -0,0 +1 @@ +bun: can't use ORDER or LIMIT with multiple tables diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql5-171 b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-171 new file mode 100644 index 000000000..887c9a7df --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql5-171 @@ -0,0 +1 @@ +UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql8-168 b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-168 new file mode 100644 index 000000000..2b9a87225 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-168 @@ -0,0 +1 @@ +DELETE FROM `models` AS `model` WHERE (`model`.`id` = NULL) ORDER BY `id` diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql8-169 b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-169 new file mode 100644 index 000000000..b85945aaf --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-169 @@ -0,0 +1 @@ +DELETE FROM `models` AS `model` WHERE (`model`.`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql8-170 b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-170 new file mode 100644 index 000000000..7d198652f --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-170 @@ -0,0 +1 @@ +bun: can't use ORDER or LIMIT with multiple tables diff --git a/internal/dbtest/testdata/snapshots/TestQuery-mysql8-171 b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-171 new file mode 100644 index 000000000..887c9a7df --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-mysql8-171 @@ -0,0 +1 @@ +UPDATE `stories` AS `story` SET name = 'new-name' WHERE (`story`.`id` = NULL) ORDER BY `id` LIMIT 1 diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pg-168 b/internal/dbtest/testdata/snapshots/TestQuery-pg-168 new file mode 100644 index 000000000..99ac97add --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pg-168 @@ -0,0 +1 @@ +bun: order is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pg-169 b/internal/dbtest/testdata/snapshots/TestQuery-pg-169 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pg-169 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pg-170 b/internal/dbtest/testdata/snapshots/TestQuery-pg-170 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pg-170 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pg-171 b/internal/dbtest/testdata/snapshots/TestQuery-pg-171 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pg-171 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pgx-168 b/internal/dbtest/testdata/snapshots/TestQuery-pgx-168 new file mode 100644 index 000000000..99ac97add --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pgx-168 @@ -0,0 +1 @@ +bun: order is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pgx-169 b/internal/dbtest/testdata/snapshots/TestQuery-pgx-169 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pgx-169 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pgx-170 b/internal/dbtest/testdata/snapshots/TestQuery-pgx-170 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pgx-170 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-pgx-171 b/internal/dbtest/testdata/snapshots/TestQuery-pgx-171 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-pgx-171 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-sqlite-168 b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-168 new file mode 100644 index 000000000..99ac97add --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-168 @@ -0,0 +1 @@ +bun: order is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-sqlite-169 b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-169 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-169 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-sqlite-170 b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-170 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-170 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/internal/dbtest/testdata/snapshots/TestQuery-sqlite-171 b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-171 new file mode 100644 index 000000000..673a81e73 --- /dev/null +++ b/internal/dbtest/testdata/snapshots/TestQuery-sqlite-171 @@ -0,0 +1 @@ +bun: limit is not supported for current dialect diff --git a/query_base.go b/query_base.go index 8a26a4c8a..52b0c1e22 100644 --- a/query_base.go +++ b/query_base.go @@ -6,6 +6,8 @@ import ( "database/sql/driver" "errors" "fmt" + "strconv" + "strings" "time" "github.com/uptrace/bun/dialect" @@ -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 +} diff --git a/query_delete.go b/query_delete.go index 49a750cc8..c6f44475b 100644 --- a/query_delete.go +++ b/query_delete.go @@ -3,6 +3,7 @@ package bun import ( "context" "database/sql" + "errors" "time" "github.com/uptrace/bun/dialect/feature" @@ -12,6 +13,7 @@ import ( type DeleteQuery struct { whereBaseQuery + orderLimitOffsetQuery returningQuery } @@ -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. @@ -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) diff --git a/query_select.go b/query_select.go index 5bb329143..448c02fe3 100644 --- a/query_select.go +++ b/query_select.go @@ -6,8 +6,6 @@ import ( "database/sql" "errors" "fmt" - "strconv" - "strings" "sync" "github.com/uptrace/bun/dialect" @@ -25,14 +23,12 @@ type union struct { type SelectQuery struct { whereBaseQuery idxHintsQuery + orderLimitOffsetQuery distinctOn []schema.QueryWithArgs joins []joinQuery group []schema.QueryWithArgs having []schema.QueryWithArgs - order []schema.QueryWithArgs - limit int32 - offset int32 selFor schema.QueryWithArgs union []union @@ -279,46 +275,22 @@ func (q *SelectQuery) Having(having string, args ...interface{}) *SelectQuery { } func (q *SelectQuery) Order(orders ...string) *SelectQuery { - 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)) - } - } + q.addOrder(orders...) return q } func (q *SelectQuery) OrderExpr(query string, args ...interface{}) *SelectQuery { - q.order = append(q.order, schema.SafeQuery(query, args)) + q.addOrderExpr(query, args...) return q } func (q *SelectQuery) Limit(n int) *SelectQuery { - q.limit = int32(n) + q.setLimit(n) return q } func (q *SelectQuery) Offset(n int) *SelectQuery { - q.offset = int32(n) + q.setOffset(n) return q } @@ -615,35 +587,9 @@ func (q *SelectQuery) appendQuery( return nil, err } - 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) - } + b, err = q.appendLimitOffset(fmter, b) + if err != nil { + return nil, err } if !q.selFor.IsZero() { @@ -782,31 +728,6 @@ func (q *SelectQuery) appendTables(fmter schema.Formatter, b []byte) (_ []byte, return q.appendTablesWithAlias(fmter, b) } -func (q *SelectQuery) 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 *SelectQuery) Rows(ctx context.Context) (*sql.Rows, error) { diff --git a/query_update.go b/query_update.go index e56ba20d1..7e68e8b8f 100644 --- a/query_update.go +++ b/query_update.go @@ -15,6 +15,7 @@ import ( type UpdateQuery struct { whereBaseQuery + orderLimitOffsetQuery returningQuery customValueQuery setQuery @@ -200,6 +201,34 @@ func (q *UpdateQuery) WhereAllWithDeleted() *UpdateQuery { return q } +// ------------------------------------------------------------------------------ +func (q *UpdateQuery) Order(orders ...string) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrder(orders...) + return q +} + +func (q *UpdateQuery) OrderExpr(query string, args ...interface{}) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + q.err = errors.New("bun: order is not supported for current dialect") + return q + } + q.addOrderExpr(query, args...) + return q +} + +func (q *UpdateQuery) Limit(n int) *UpdateQuery { + if !q.hasFeature(feature.UpdateOrderLimit) { + 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. @@ -278,6 +307,16 @@ func (q *UpdateQuery) AppendQuery(fmter schema.Formatter, b []byte) (_ []byte, e return nil, err } + 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)