Skip to content

Commit 17ffeb7

Browse files
authored
Merge pull request #544 from jeffgbutler/limit-delete-update
Support "limit" and "order by" on DELETE and UPDATE Statements
2 parents 95d866a + 5c2f5b2 commit 17ffeb7

33 files changed

+1067
-295
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
This log will detail notable changes to MyBatis Dynamic SQL. Full details are available on the GitHub milestone pages.
44

5+
## Release 1.5.0 - Unreleased
6+
7+
GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/milestone/12](https://github.com/mybatis/mybatis-dynamic-sql/milestone/12)
8+
9+
Added:
10+
11+
1. Added support for specifying "limit" and "order by" on the DELETE and UPDATE statements. Not all databases support
12+
this SQL extension, and different databases have different levels of support. For example, MySQL/MariaDB have full
13+
support but HSQLDB only supports limit as an extension to the WHERE clause. If you choose to use this new capability,
14+
please test to make sure it is supported in your database. ([#544](https://github.com/mybatis/mybatis-dynamic-sql/pull/544))
15+
16+
517
## Release 1.4.1 - October 7, 2022
618

719
GitHub milestone: [https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+](https://github.com/mybatis/mybatis-dynamic-sql/issues?q=milestone%3A1.4.1+)

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,18 @@
316316
<version>42.5.0</version>
317317
<scope>test</scope>
318318
</dependency>
319+
<dependency>
320+
<groupId>org.testcontainers</groupId>
321+
<artifactId>mariadb</artifactId>
322+
<version>${test.containers.version}</version>
323+
<scope>test</scope>
324+
</dependency>
325+
<dependency>
326+
<groupId>org.mariadb.jdbc</groupId>
327+
<artifactId>mariadb-java-client</artifactId>
328+
<version>3.0.8</version>
329+
<scope>test</scope>
330+
</dependency>
319331
</dependencies>
320332

321333
</project>

src/main/java/org/mybatis/dynamic/sql/select/OrderByModel.java renamed to src/main/java/org/mybatis/dynamic/sql/common/OrderByModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package org.mybatis.dynamic.sql.select;
16+
package org.mybatis.dynamic.sql.common;
1717

1818
import java.util.ArrayList;
1919
import java.util.Collection;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2016-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.mybatis.dynamic.sql.common;
17+
18+
import java.util.stream.Collectors;
19+
20+
import org.mybatis.dynamic.sql.SortSpecification;
21+
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
22+
23+
public class OrderByRenderer {
24+
public FragmentAndParameters render(OrderByModel orderByModel) {
25+
String phrase = orderByModel.mapColumns(this::calculateOrderByPhrase)
26+
.collect(Collectors.joining(", ", "order by ", "")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
27+
return FragmentAndParameters.withFragment(phrase).build();
28+
}
29+
30+
private String calculateOrderByPhrase(SortSpecification column) {
31+
String phrase = column.orderByName();
32+
if (column.isDescending()) {
33+
phrase = phrase + " DESC"; //$NON-NLS-1$
34+
}
35+
return phrase;
36+
}
37+
}

src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
*/
1616
package org.mybatis.dynamic.sql.delete;
1717

18+
import java.util.Arrays;
19+
import java.util.Collection;
1820
import java.util.Objects;
1921
import java.util.function.Consumer;
2022
import java.util.function.Function;
2123

2224
import org.jetbrains.annotations.NotNull;
25+
import org.mybatis.dynamic.sql.SortSpecification;
2326
import org.mybatis.dynamic.sql.SqlTable;
27+
import org.mybatis.dynamic.sql.common.OrderByModel;
2428
import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
2529
import org.mybatis.dynamic.sql.util.Buildable;
2630
import org.mybatis.dynamic.sql.where.AbstractWhereDSL;
@@ -35,6 +39,8 @@ public class DeleteDSL<R> extends AbstractWhereSupport<DeleteDSL<R>.DeleteWhereB
3539
private final String tableAlias;
3640
private DeleteWhereBuilder whereBuilder;
3741
private final StatementConfiguration statementConfiguration = new StatementConfiguration();
42+
private Long limit;
43+
private OrderByModel orderByModel;
3844

3945
private DeleteDSL(SqlTable table, String tableAlias, Function<DeleteModel, R> adapterFunction) {
4046
this.table = Objects.requireNonNull(table);
@@ -50,6 +56,20 @@ public DeleteWhereBuilder where() {
5056
return whereBuilder;
5157
}
5258

59+
public DeleteDSL<R> limit(long limit) {
60+
this.limit = limit;
61+
return this;
62+
}
63+
64+
public DeleteDSL<R> orderBy(SortSpecification... columns) {
65+
return orderBy(Arrays.asList(columns));
66+
}
67+
68+
public DeleteDSL<R> orderBy(Collection<SortSpecification> columns) {
69+
orderByModel = OrderByModel.of(columns);
70+
return this;
71+
}
72+
5373
/**
5474
* WARNING! Calling this method could result in a delete statement that deletes
5575
* all rows in a table.
@@ -60,7 +80,9 @@ public DeleteWhereBuilder where() {
6080
@Override
6181
public R build() {
6282
DeleteModel.Builder deleteModelBuilder = DeleteModel.withTable(table)
63-
.withTableAlias(tableAlias);
83+
.withTableAlias(tableAlias)
84+
.withLimit(limit)
85+
.withOrderByModel(orderByModel);
6486
if (whereBuilder != null) {
6587
deleteModelBuilder.withWhereModel(whereBuilder.buildWhereModel());
6688
}
@@ -93,6 +115,19 @@ private DeleteWhereBuilder() {
93115
super(statementConfiguration);
94116
}
95117

118+
public DeleteDSL<R> limit(long limit) {
119+
return DeleteDSL.this.limit(limit);
120+
}
121+
122+
public DeleteDSL<R> orderBy(SortSpecification... columns) {
123+
return orderBy(Arrays.asList(columns));
124+
}
125+
126+
public DeleteDSL<R> orderBy(Collection<SortSpecification> columns) {
127+
orderByModel = OrderByModel.of(columns);
128+
return DeleteDSL.this;
129+
}
130+
96131
@NotNull
97132
@Override
98133
public R build() {

src/main/java/org/mybatis/dynamic/sql/delete/DeleteModel.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.jetbrains.annotations.NotNull;
2222
import org.mybatis.dynamic.sql.SqlTable;
23+
import org.mybatis.dynamic.sql.common.OrderByModel;
2324
import org.mybatis.dynamic.sql.delete.render.DeleteRenderer;
2425
import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
2526
import org.mybatis.dynamic.sql.render.RenderingStrategy;
@@ -29,11 +30,15 @@ public class DeleteModel {
2930
private final SqlTable table;
3031
private final String tableAlias;
3132
private final WhereModel whereModel;
33+
private final Long limit;
34+
private final OrderByModel orderByModel;
3235

3336
private DeleteModel(Builder builder) {
3437
table = Objects.requireNonNull(builder.table);
3538
whereModel = builder.whereModel;
3639
tableAlias = builder.tableAlias;
40+
limit = builder.limit;
41+
orderByModel = builder.orderByModel;
3742
}
3843

3944
public SqlTable table() {
@@ -48,6 +53,14 @@ public Optional<WhereModel> whereModel() {
4853
return Optional.ofNullable(whereModel);
4954
}
5055

56+
public Optional<Long> limit() {
57+
return Optional.ofNullable(limit);
58+
}
59+
60+
public Optional<OrderByModel> orderByModel() {
61+
return Optional.ofNullable(orderByModel);
62+
}
63+
5164
@NotNull
5265
public DeleteStatementProvider render(RenderingStrategy renderingStrategy) {
5366
return DeleteRenderer.withDeleteModel(this)
@@ -64,6 +77,8 @@ public static class Builder {
6477
private SqlTable table;
6578
private String tableAlias;
6679
private WhereModel whereModel;
80+
private Long limit;
81+
private OrderByModel orderByModel;
6782

6883
public Builder withTable(SqlTable table) {
6984
this.table = table;
@@ -80,6 +95,16 @@ public Builder withWhereModel(WhereModel whereModel) {
8095
return this;
8196
}
8297

98+
public Builder withLimit(Long limit) {
99+
this.limit = limit;
100+
return this;
101+
}
102+
103+
public Builder withOrderByModel(OrderByModel orderByModel) {
104+
this.orderByModel = orderByModel;
105+
return this;
106+
}
107+
83108
public DeleteModel build() {
84109
return new DeleteModel(this);
85110
}

src/main/java/org/mybatis/dynamic/sql/delete/render/DeleteRenderer.java

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,28 @@
1515
*/
1616
package org.mybatis.dynamic.sql.delete.render;
1717

18-
import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore;
19-
2018
import java.util.Objects;
2119
import java.util.Optional;
2220
import java.util.concurrent.atomic.AtomicInteger;
21+
import java.util.stream.Collectors;
2322

2423
import org.mybatis.dynamic.sql.SqlTable;
24+
import org.mybatis.dynamic.sql.common.OrderByModel;
25+
import org.mybatis.dynamic.sql.common.OrderByRenderer;
2526
import org.mybatis.dynamic.sql.delete.DeleteModel;
2627
import org.mybatis.dynamic.sql.render.ExplicitTableAliasCalculator;
2728
import org.mybatis.dynamic.sql.render.RenderingStrategy;
2829
import org.mybatis.dynamic.sql.render.TableAliasCalculator;
30+
import org.mybatis.dynamic.sql.util.FragmentAndParameters;
31+
import org.mybatis.dynamic.sql.util.FragmentCollector;
2932
import org.mybatis.dynamic.sql.where.WhereModel;
30-
import org.mybatis.dynamic.sql.where.render.WhereClauseProvider;
3133
import org.mybatis.dynamic.sql.where.render.WhereRenderer;
3234

3335
public class DeleteRenderer {
3436
private final DeleteModel deleteModel;
3537
private final RenderingStrategy renderingStrategy;
3638
private final TableAliasCalculator tableAliasCalculator;
39+
private final AtomicInteger sequence = new AtomicInteger(1);
3740

3841
private DeleteRenderer(Builder builder) {
3942
deleteModel = Objects.requireNonNull(builder.deleteModel);
@@ -44,46 +47,68 @@ private DeleteRenderer(Builder builder) {
4447
}
4548

4649
public DeleteStatementProvider render() {
47-
return deleteModel.whereModel()
48-
.flatMap(this::renderWhereClause)
49-
.map(this::renderWithWhereClause)
50-
.orElseGet(this::renderWithoutWhereClause);
51-
}
50+
FragmentCollector fragmentCollector = new FragmentCollector();
5251

53-
private DeleteStatementProvider renderWithWhereClause(WhereClauseProvider whereClauseProvider) {
54-
return DefaultDeleteStatementProvider.withDeleteStatement(calculateDeleteStatement(whereClauseProvider))
55-
.withParameters(whereClauseProvider.getParameters())
56-
.build();
52+
fragmentCollector.add(calculateDeleteStatementStart());
53+
calculateWhereClause().ifPresent(fragmentCollector::add);
54+
calculateOrderByClause().ifPresent(fragmentCollector::add);
55+
calculateLimitClause().ifPresent(fragmentCollector::add);
56+
57+
return toDeleteStatementProvider(fragmentCollector);
5758
}
5859

59-
private String calculateDeleteStatement(WhereClauseProvider whereClause) {
60-
return calculateDeleteStatement()
61-
+ spaceBefore(whereClause.getWhereClause());
60+
private DeleteStatementProvider toDeleteStatementProvider(FragmentCollector fragmentCollector) {
61+
return DefaultDeleteStatementProvider
62+
.withDeleteStatement(fragmentCollector.fragments().collect(Collectors.joining(" "))) //$NON-NLS-1$
63+
.withParameters(fragmentCollector.parameters())
64+
.build();
6265
}
6366

64-
private String calculateDeleteStatement() {
67+
private FragmentAndParameters calculateDeleteStatementStart() {
6568
SqlTable table = deleteModel.table();
6669
String tableName = table.tableNameAtRuntime();
6770
String aliasedTableName = tableAliasCalculator.aliasForTable(table)
6871
.map(a -> tableName + " " + a).orElse(tableName); //$NON-NLS-1$
6972

70-
return "delete from" + spaceBefore(aliasedTableName); //$NON-NLS-1$
73+
return FragmentAndParameters.withFragment("delete from " + aliasedTableName) //$NON-NLS-1$
74+
.build();
7175
}
7276

73-
private DeleteStatementProvider renderWithoutWhereClause() {
74-
return DefaultDeleteStatementProvider.withDeleteStatement(calculateDeleteStatement())
75-
.build();
77+
private Optional<FragmentAndParameters> calculateWhereClause() {
78+
return deleteModel.whereModel().flatMap(this::renderWhereClause);
7679
}
7780

78-
private Optional<WhereClauseProvider> renderWhereClause(WhereModel whereModel) {
81+
private Optional<FragmentAndParameters> renderWhereClause(WhereModel whereModel) {
7982
return WhereRenderer.withWhereModel(whereModel)
8083
.withRenderingStrategy(renderingStrategy)
81-
.withSequence(new AtomicInteger(1))
84+
.withSequence(sequence)
8285
.withTableAliasCalculator(tableAliasCalculator)
8386
.build()
8487
.render();
8588
}
8689

90+
private Optional<FragmentAndParameters> calculateLimitClause() {
91+
return deleteModel.limit().map(this::renderLimitClause);
92+
}
93+
94+
private FragmentAndParameters renderLimitClause(Long limit) {
95+
String mapKey = RenderingStrategy.formatParameterMapKey(sequence);
96+
String jdbcPlaceholder =
97+
renderingStrategy.getFormattedJdbcPlaceholder(RenderingStrategy.DEFAULT_PARAMETER_PREFIX, mapKey);
98+
99+
return FragmentAndParameters.withFragment("limit " + jdbcPlaceholder) //$NON-NLS-1$
100+
.withParameter(mapKey, limit)
101+
.build();
102+
}
103+
104+
private Optional<FragmentAndParameters> calculateOrderByClause() {
105+
return deleteModel.orderByModel().map(this::renderOrderByClause);
106+
}
107+
108+
private FragmentAndParameters renderOrderByClause(OrderByModel orderByModel) {
109+
return new OrderByRenderer().render(orderByModel);
110+
}
111+
87112
public static Builder withDeleteModel(DeleteModel deleteModel) {
88113
return new Builder().withDeleteModel(deleteModel);
89114
}

src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.mybatis.dynamic.sql.SortSpecification;
3030
import org.mybatis.dynamic.sql.SqlTable;
3131
import org.mybatis.dynamic.sql.TableExpression;
32+
import org.mybatis.dynamic.sql.common.OrderByModel;
3233
import org.mybatis.dynamic.sql.configuration.StatementConfiguration;
3334
import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer;
3435
import org.mybatis.dynamic.sql.util.Buildable;
@@ -152,6 +153,10 @@ private List<QueryExpressionModel> buildModels() {
152153
}
153154

154155
private PagingModel buildPagingModel() {
156+
if (limit == null && offset == null && fetchFirstRows == null) {
157+
return null;
158+
}
159+
155160
return new PagingModel.Builder()
156161
.withLimit(limit)
157162
.withOffset(offset)
@@ -161,7 +166,7 @@ private PagingModel buildPagingModel() {
161166

162167
public class LimitFinisher implements Buildable<R> {
163168
public OffsetFinisher offset(long offset) {
164-
SelectDSL.this.offset = offset;
169+
SelectDSL.this.offset(offset);
165170
return new OffsetFinisher();
166171
}
167172

@@ -182,7 +187,7 @@ public R build() {
182187

183188
public class OffsetFirstFinisher implements Buildable<R> {
184189
public FetchFirstFinisher fetchFirst(long fetchFirstRows) {
185-
SelectDSL.this.fetchFirstRows = fetchFirstRows;
190+
SelectDSL.this.fetchFirst(fetchFirstRows);
186191
return new FetchFirstFinisher();
187192
}
188193

src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.stream.Stream;
2424

2525
import org.jetbrains.annotations.NotNull;
26+
import org.mybatis.dynamic.sql.common.OrderByModel;
2627
import org.mybatis.dynamic.sql.exception.InvalidSqlException;
2728
import org.mybatis.dynamic.sql.render.RenderingStrategy;
2829
import org.mybatis.dynamic.sql.select.render.SelectRenderer;

0 commit comments

Comments
 (0)