diff --git a/CHANGELOG.md b/CHANGELOG.md index 324cff2e8..3c12f44da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,10 @@ Runtime behavior changes: rendering if a null value is passed in - The JOIN syntax is updated and now allows full boolean expressions like a WHERE clause. The prior JOIN syntax is deprecated and will be removed in a future release. +- Add support for locking options in select statements (for update, for share, etc.) This is not an abstraction of + these concepts for different databases it simply adds known clauses to a generated SQL statement. You should always + test to make sure these functions work in your target database. Currently, we support, and test, the options + supported by PostgreSQL. ## Release 1.5.2 - June 3, 2024 diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java index 264e8046e..c8375bbd3 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractHavingStarter.java @@ -25,14 +25,14 @@ import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.VisitableCondition; -public abstract class AbstractHavingStarter> { +public interface AbstractHavingStarter> { - public F having(BindableColumn column, VisitableCondition condition, + default F having(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { return having(column, condition, Arrays.asList(subCriteria)); } - public F having(BindableColumn column, VisitableCondition condition, + default F having(BindableColumn column, VisitableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) @@ -42,11 +42,11 @@ public F having(BindableColumn column, VisitableCondition condition, return initialize(sqlCriterion); } - public F having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + default F having(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { return having(initialCriterion, Arrays.asList(subCriteria)); } - public F having(SqlCriterion initialCriterion, List subCriteria) { + default F having(SqlCriterion initialCriterion, List subCriteria) { SqlCriterion sqlCriterion = new CriteriaGroup.Builder() .withInitialCriterion(initialCriterion) .withSubCriteria(subCriteria) @@ -55,9 +55,9 @@ public F having(SqlCriterion initialCriterion, List subCrite return initialize(sqlCriterion); } - protected abstract F having(); + F having(); - public F applyHaving(HavingApplier havingApplier) { + default F applyHaving(HavingApplier havingApplier) { F finisher = having(); havingApplier.accept(finisher); return finisher; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java index 34be15f62..58ca01184 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/HavingDSL.java @@ -17,11 +17,11 @@ import org.mybatis.dynamic.sql.util.Buildable; -public class HavingDSL extends AbstractHavingStarter { +public class HavingDSL implements AbstractHavingStarter { private final StandaloneHavingFinisher havingFinisher = new StandaloneHavingFinisher(); @Override - protected StandaloneHavingFinisher having() { + public StandaloneHavingFinisher having() { return havingFinisher; } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java index 308107c30..63ceb4e0a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java @@ -29,8 +29,7 @@ import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConfigurableStatement; -public class MultiSelectDSL implements Buildable, ConfigurableStatement, - PagingDSL { +public class MultiSelectDSL implements Buildable, ConfigurableStatement { private final List unionQueries = new ArrayList<>(); private final SelectModel initialSelect; private @Nullable OrderByModel orderByModel; @@ -62,22 +61,31 @@ public MultiSelectDSL orderBy(Collection columns) { return this; } - @Override - public LimitFinisher limitWhenPresent(@Nullable Long limit) { + public LimitFinisher limit(long limit) { + return limitWhenPresent(limit); + } + + public LimitFinisher limitWhenPresent(@Nullable Long limit) { this.limit = limit; - return new LocalLimitFinisher(); + return new LimitFinisher(); } - @Override - public OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { + public OffsetFirstFinisher offset(long offset) { + return offsetWhenPresent(offset); + } + + public OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { this.offset = offset; - return new LocalOffsetFirstFinisher(); + return new OffsetFirstFinisher(); } - @Override - public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> this; + return new FetchFirstFinisher(); } @Override @@ -105,25 +113,40 @@ public MultiSelectDSL configureStatement(Consumer consum return this; } - abstract class BaseBuildable implements Buildable { + public class OffsetFirstFinisher implements Buildable { + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + MultiSelectDSL.this.fetchFirstRows = fetchFirstRows; + return new FetchFirstFinisher(); + } + @Override public MultiSelectModel build() { return MultiSelectDSL.this.build(); } } - class LocalOffsetFirstFinisher extends BaseBuildable implements OffsetFirstFinisher { + public class LimitFinisher implements Buildable { + public MultiSelectDSL offset(long offset) { + return offsetWhenPresent(offset); + } + + public MultiSelectDSL offsetWhenPresent(@Nullable Long offset) { + MultiSelectDSL.this.offset = offset; + return MultiSelectDSL.this; + } + @Override - public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { - MultiSelectDSL.this.fetchFirstRows = fetchFirstRows; - return () -> MultiSelectDSL.this; + public MultiSelectModel build() { + return MultiSelectDSL.this.build(); } } - class LocalLimitFinisher extends BaseBuildable implements LimitFinisher { - @Override - public Buildable offsetWhenPresent(Long offset) { - MultiSelectDSL.this.offset = offset; + public class FetchFirstFinisher { + public MultiSelectDSL rowsOnly() { return MultiSelectDSL.this; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/PagingDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/PagingDSL.java deleted file mode 100644 index 7c6f004fd..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/PagingDSL.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.mybatis.dynamic.sql.select; - -import org.mybatis.dynamic.sql.util.Buildable; - -public interface PagingDSL { - default LimitFinisher limit(long limit) { - return limitWhenPresent(limit); - } - - LimitFinisher limitWhenPresent(Long limit); - - default OffsetFirstFinisher offset(long offset) { - return offsetWhenPresent(offset); - } - - OffsetFirstFinisher offsetWhenPresent(Long offset); - - default FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return fetchFirstWhenPresent(fetchFirstRows); - } - - FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows); - - interface LimitFinisher extends Buildable { - default Buildable offset(long offset) { - return offsetWhenPresent(offset); - } - - Buildable offsetWhenPresent(Long offset); - } - - interface OffsetFirstFinisher extends Buildable { - default FetchFirstFinisher fetchFirst(long fetchFirstRows) { - return fetchFirstWhenPresent(fetchFirstRows); - } - - FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows); - } - - @FunctionalInterface - interface FetchFirstFinisher { - Buildable rowsOnly(); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java index 0964ee3b3..afb6cda0c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -44,7 +44,7 @@ public class QueryExpressionDSL extends AbstractQueryExpressionDSL.QueryExpressionWhereBuilder, QueryExpressionDSL> - implements Buildable, PagingDSL { + implements Buildable, SelectDSLOperations { private final @Nullable String connector; private final SelectDSL selectDSL; @@ -196,23 +196,13 @@ protected QueryExpressionModel buildModel() { } @Override - public PagingDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { - return selectDSL.limitWhenPresent(limit); - } - - @Override - public PagingDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { - return selectDSL.offsetWhenPresent(offset); - } - - @Override - public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { - return selectDSL.fetchFirstWhenPresent(fetchFirstRows); + protected QueryExpressionDSL getThis() { + return this; } @Override - protected QueryExpressionDSL getThis() { - return this; + public SelectDSL getSelectDSL() { + return selectDSL; } public static class FromGatherer { @@ -277,7 +267,7 @@ public FromGatherer build() { } public class QueryExpressionWhereBuilder extends AbstractWhereFinisher - implements Buildable, PagingDSL { + implements Buildable, SelectDSLOperations { private QueryExpressionWhereBuilder() { super(QueryExpressionDSL.this); } @@ -306,21 +296,6 @@ public GroupByFinisher groupBy(Collection columns) { return QueryExpressionDSL.this.groupBy(columns); } - @Override - public PagingDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { - return QueryExpressionDSL.this.limitWhenPresent(limit); - } - - @Override - public PagingDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { - return QueryExpressionDSL.this.offsetWhenPresent(offset); - } - - @Override - public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirstWhenPresent(fetchFirstRows); - } - @Override public R build() { return QueryExpressionDSL.this.build(); @@ -331,6 +306,11 @@ protected QueryExpressionWhereBuilder getThis() { return this; } + @Override + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); + } + protected EmbeddedWhereModel buildWhereModel() { return super.buildModel(); } @@ -358,7 +338,7 @@ public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableC public class JoinSpecificationFinisher extends AbstractBooleanExpressionDSL implements AbstractWhereStarter, Buildable, - PagingDSL { + SelectDSLOperations { private final TableExpression table; private final JoinType joinType; @@ -487,28 +467,18 @@ public SelectDSL orderBy(Collection columns) { } @Override - public PagingDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { - return QueryExpressionDSL.this.limitWhenPresent(limit); - } - - @Override - public PagingDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { - return QueryExpressionDSL.this.offsetWhenPresent(offset); - } - - @Override - public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirstWhenPresent(fetchFirstRows); + protected JoinSpecificationFinisher getThis() { + return this; } @Override - protected JoinSpecificationFinisher getThis() { - return this; + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); } } - public class GroupByFinisher extends AbstractHavingStarter - implements Buildable, PagingDSL { + public class GroupByFinisher implements AbstractHavingStarter, + Buildable, SelectDSLOperations { public SelectDSL orderBy(SortSpecification... columns) { return orderBy(Arrays.asList(columns)); } @@ -531,23 +501,13 @@ public UnionBuilder unionAll() { } @Override - public PagingDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { - return QueryExpressionDSL.this.limitWhenPresent(limit); - } - - @Override - public PagingDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { - return QueryExpressionDSL.this.offsetWhenPresent(offset); + public QueryExpressionHavingBuilder having() { + return QueryExpressionDSL.this.having(); } @Override - public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirstWhenPresent(fetchFirstRows); - } - - @Override - public QueryExpressionHavingBuilder having() { - return QueryExpressionDSL.this.having(); + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); } } @@ -585,22 +545,7 @@ public FromGatherer selectDistinct(List selectList) { } public class QueryExpressionHavingBuilder extends AbstractHavingFinisher - implements Buildable, PagingDSL { - - @Override - public PagingDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { - return QueryExpressionDSL.this.limitWhenPresent(limit); - } - - @Override - public PagingDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { - return QueryExpressionDSL.this.offsetWhenPresent(offset); - } - - @Override - public PagingDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { - return QueryExpressionDSL.this.fetchFirstWhenPresent(fetchFirstRows); - } + implements Buildable, SelectDSLOperations { public SelectDSL orderBy(SortSpecification... columns) { return orderBy(Arrays.asList(columns)); @@ -631,5 +576,10 @@ protected QueryExpressionHavingBuilder getThis() { protected HavingModel buildHavingModel() { return super.buildModel(); } + + @Override + public SelectDSL getSelectDSL() { + return QueryExpressionDSL.this.getSelectDSL(); + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java index 7f1fd239b..6b52d3c3f 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java @@ -32,6 +32,7 @@ import org.mybatis.dynamic.sql.select.QueryExpressionDSL.FromGatherer; import org.mybatis.dynamic.sql.util.Buildable; import org.mybatis.dynamic.sql.util.ConfigurableStatement; +import org.mybatis.dynamic.sql.util.Validator; /** * Implements a SQL DSL for building select statements. @@ -41,7 +42,7 @@ * @param * the type of model produced by this builder, typically SelectModel */ -public class SelectDSL implements Buildable, ConfigurableStatement>, PagingDSL { +public class SelectDSL implements Buildable, ConfigurableStatement> { private final Function adapterFunction; private final List> queryExpressions = new ArrayList<>(); @@ -50,6 +51,8 @@ public class SelectDSL implements Buildable, ConfigurableStatement adapterFunction) { this.adapterFunction = Objects.requireNonNull(adapterFunction); @@ -107,19 +110,67 @@ void orderBy(Collection columns) { orderByModel = OrderByModel.of(columns); } - public LimitFinisher limitWhenPresent(@Nullable Long limit) { + public LimitFinisher limit(long limit) { + return limitWhenPresent(limit); + } + + public LimitFinisher limitWhenPresent(@Nullable Long limit) { this.limit = limit; - return new LocalLimitFinisher(); + return new LimitFinisher(); + } + + public OffsetFirstFinisher offset(long offset) { + return offsetWhenPresent(offset); } - public OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { + public OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { this.offset = offset; - return new LocalOffsetFirstFinisher(); + return new OffsetFirstFinisher(); + } + + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); } - public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> this; + return new FetchFirstFinisher(); + } + + public SelectDSL forUpdate() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for update"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forNoKeyUpdate() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for no key update"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forShare() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for share"; //$NON-NLS-1$ + return this; + } + + public SelectDSL forKeyShare() { + Validator.assertNull(forClause, "ERROR.48"); //$NON-NLS-1$ + forClause = "for key share"; //$NON-NLS-1$ + return this; + } + + public SelectDSL skipLocked() { + Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$ + waitClause = "skip locked"; //$NON-NLS-1$ + return this; + } + + public SelectDSL nowait() { + Validator.assertNull(waitClause, "ERROR.49"); //$NON-NLS-1$ + waitClause = "nowait"; //$NON-NLS-1$ + return this; } @Override @@ -134,6 +185,8 @@ public R build() { .withOrderByModel(orderByModel) .withPagingModel(buildPagingModel().orElse(null)) .withStatementConfiguration(statementConfiguration) + .withForClause(forClause) + .withWaitClause(waitClause) .build(); return adapterFunction.apply(selectModel); } @@ -152,25 +205,50 @@ private Optional buildPagingModel() { .build(); } - abstract class BaseBuildable implements Buildable { + public class OffsetFirstFinisher implements SelectDSLForAndWaitOperations, Buildable { + public FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return fetchFirstWhenPresent(fetchFirstRows); + } + + public FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + SelectDSL.this.fetchFirstRows = fetchFirstRows; + return new FetchFirstFinisher(); + } + + @Override + public SelectDSL getSelectDSL() { + return SelectDSL.this; + } + @Override public R build() { return SelectDSL.this.build(); } } - class LocalOffsetFirstFinisher extends BaseBuildable implements OffsetFirstFinisher { + public class LimitFinisher implements SelectDSLForAndWaitOperations, Buildable { + public SelectDSL offset(long offset) { + return offsetWhenPresent(offset); + } + + public SelectDSL offsetWhenPresent(@Nullable Long offset) { + SelectDSL.this.offset = offset; + return SelectDSL.this; + } + @Override - public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { - SelectDSL.this.fetchFirstRows = fetchFirstRows; - return () -> SelectDSL.this; + public SelectDSL getSelectDSL() { + return SelectDSL.this; } - } - class LocalLimitFinisher extends BaseBuildable implements LimitFinisher { @Override - public Buildable offsetWhenPresent(Long offset) { - SelectDSL.this.offset = offset; + public R build() { + return SelectDSL.this.build(); + } + } + + public class FetchFirstFinisher { + public SelectDSL rowsOnly() { return SelectDSL.this; } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java new file mode 100644 index 000000000..e4dc45d97 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLForAndWaitOperations.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.select; + +public interface SelectDSLForAndWaitOperations { + default SelectDSL forUpdate() { + return getSelectDSL().forUpdate(); + } + + default SelectDSL forNoKeyUpdate() { + return getSelectDSL().forNoKeyUpdate(); + } + + default SelectDSL forShare() { + return getSelectDSL().forShare(); + } + + default SelectDSL forKeyShare() { + return getSelectDSL().forKeyShare(); + } + + default SelectDSL skipLocked() { + return getSelectDSL().skipLocked(); + } + + default SelectDSL nowait() { + return getSelectDSL().nowait(); + } + + /** + * Gain access to the SelectDSL instance. + * + *

This is a leak of an implementation detail into the public API. The tradeoff is that it + * significantly reduces copy/paste code of SelectDSL methods into all the different inner classes of + * QueryExpressionDSL where they would be needed. + * + * @return the SelectDSL instance associated with this interface instance + */ + SelectDSL getSelectDSL(); +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java new file mode 100644 index 000000000..02f862c3b --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSLOperations.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.mybatis.dynamic.sql.select; + +import org.jspecify.annotations.Nullable; + +public interface SelectDSLOperations extends SelectDSLForAndWaitOperations { + default SelectDSL.LimitFinisher limit(long limit) { + return getSelectDSL().limit(limit); + } + + default SelectDSL.LimitFinisher limitWhenPresent(@Nullable Long limit) { + return getSelectDSL().limitWhenPresent(limit); + } + + default SelectDSL.OffsetFirstFinisher offset(long offset) { + return getSelectDSL().offset(offset); + } + + default SelectDSL.OffsetFirstFinisher offsetWhenPresent(@Nullable Long offset) { + return getSelectDSL().offsetWhenPresent(offset); + } + + default SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { + return getSelectDSL().fetchFirst(fetchFirstRows); + } + + default SelectDSL.FetchFirstFinisher fetchFirstWhenPresent(@Nullable Long fetchFirstRows) { + return getSelectDSL().fetchFirstWhenPresent(fetchFirstRows); + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java b/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java index fd4eecc0c..ec277760b 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectModel.java @@ -18,8 +18,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.render.RenderingStrategy; import org.mybatis.dynamic.sql.select.render.SelectRenderer; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; @@ -27,17 +29,29 @@ public class SelectModel extends AbstractSelectModel { private final List queryExpressions; + private final @Nullable String forClause; + private final @Nullable String waitClause; private SelectModel(Builder builder) { super(builder); queryExpressions = Objects.requireNonNull(builder.queryExpressions); Validator.assertNotEmpty(queryExpressions, "ERROR.14"); //$NON-NLS-1$ + forClause = builder.forClause; + waitClause = builder.waitClause; } public Stream queryExpressions() { return queryExpressions.stream(); } + public Optional forClause() { + return Optional.ofNullable(forClause); + } + + public Optional waitClause() { + return Optional.ofNullable(waitClause); + } + public SelectStatementProvider render(RenderingStrategy renderingStrategy) { return SelectRenderer.withSelectModel(this) .withRenderingStrategy(renderingStrategy) @@ -51,6 +65,8 @@ public static Builder withQueryExpressions(List queryExpre public static class Builder extends AbstractBuilder { private final List queryExpressions = new ArrayList<>(); + private @Nullable String forClause; + private @Nullable String waitClause; public Builder withQueryExpression(QueryExpressionModel queryExpression) { this.queryExpressions.add(queryExpression); @@ -62,6 +78,16 @@ public Builder withQueryExpressions(List queryExpressions) return this; } + public Builder withForClause(@Nullable String forClause) { + this.forClause = forClause; + return this; + } + + public Builder withWaitClause(@Nullable String waitClause) { + this.waitClause = waitClause; + return this; + } + @Override protected Builder getThis() { return this; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java index 6c4eb82ad..359e9c1f0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/SubQueryRenderer.java @@ -16,7 +16,6 @@ package org.mybatis.dynamic.sql.select.render; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; @@ -48,8 +47,21 @@ public FragmentAndParameters render() { .map(this::renderQueryExpression) .collect(FragmentCollector.collect()); - renderOrderBy().ifPresent(fragmentCollector::add); - renderPagingModel().ifPresent(fragmentCollector::add); + selectModel.orderByModel() + .map(this::renderOrderBy) + .ifPresent(fragmentCollector::add); + + selectModel.pagingModel() + .map(this::renderPagingModel) + .ifPresent(fragmentCollector::add); + + selectModel.forClause() + .map(FragmentAndParameters::fromFragment) + .ifPresent(fragmentCollector::add); + + selectModel.waitClause() + .map(FragmentAndParameters::fromFragment) + .ifPresent(fragmentCollector::add); return fragmentCollector.toFragmentAndParameters(Collectors.joining(" ", prefix, suffix)); //$NON-NLS-1$ } @@ -61,18 +73,10 @@ private FragmentAndParameters renderQueryExpression(QueryExpressionModel queryEx .render(); } - private Optional renderOrderBy() { - return selectModel.orderByModel().map(this::renderOrderBy); - } - private FragmentAndParameters renderOrderBy(OrderByModel orderByModel) { return new OrderByRenderer(renderingContext).render(orderByModel); } - private Optional renderPagingModel() { - return selectModel.pagingModel().map(this::renderPagingModel); - } - private FragmentAndParameters renderPagingModel(PagingModel pagingModel) { return new PagingModelRenderer.Builder() .withPagingModel(pagingModel) diff --git a/src/main/java/org/mybatis/dynamic/sql/util/Validator.java b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java index 588f6e372..7563bfc70 100644 --- a/src/main/java/org/mybatis/dynamic/sql/util/Validator.java +++ b/src/main/java/org/mybatis/dynamic/sql/util/Validator.java @@ -17,6 +17,7 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.mybatis.dynamic.sql.exception.InvalidSqlException; public class Validator { @@ -49,4 +50,10 @@ public static void assertTrue(boolean condition, String messageNumber) { public static void assertTrue(boolean condition, String messageNumber, String p1) { assertFalse(!condition, messageNumber, p1); } + + public static void assertNull(@Nullable Object object, String messageNumber) { + if (object != null) { + throw new InvalidSqlException(Messages.getString(messageNumber)); + } + } } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt index bd115230d..4c80f9d8c 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinSelectBuilder.kt @@ -76,6 +76,30 @@ class KotlinSelectBuilder(private val fromGatherer: QueryExpressionDSL.FromGathe fun unionAll(unionAll: KotlinUnionBuilder.() -> Unit): Unit = unionAll(KotlinUnionBuilder(getDsl().unionAll())) + fun forUpdate() { + getDsl().forUpdate() + } + + fun forNoKeyUpdate() { + getDsl().forNoKeyUpdate() + } + + fun forShare() { + getDsl().forShare() + } + + fun forKeyShare() { + getDsl().forKeyShare() + } + + fun skipLocked() { + getDsl().skipLocked() + } + + fun nowait() { + getDsl().nowait() + } + override fun build(): SelectModel = getDsl().build() override fun getDsl(): KQueryExpressionDSL = invalidIfNull(dsl, "ERROR.27") //$NON-NLS-1$ diff --git a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties index d99bd581e..3cf560a78 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -64,4 +64,7 @@ ERROR.44={0} conditions must contain at least one value ERROR.45=You cannot call "on" in a Kotlin join expression more than once ERROR.46=At least one join criterion must render ERROR.47=A Kotlin case statement must specify a "then" clause for every "when" clause +ERROR.48=You cannot call more than one of "forUpdate", "forNoKeyUpdate", "forShare", or "forKeyShare" in a select \ + statement +ERROR.49=You cannot call more than one of "skipLocked", or "nowait" in a select statement INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/java/examples/column/comparison/ColumnComparisonMapper.java b/src/test/java/examples/column/comparison/ColumnComparisonMapper.java index f20fc9a84..a8acf0c88 100644 --- a/src/test/java/examples/column/comparison/ColumnComparisonMapper.java +++ b/src/test/java/examples/column/comparison/ColumnComparisonMapper.java @@ -18,7 +18,6 @@ import java.util.List; import org.apache.ibatis.annotations.Result; -import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.SelectProvider; import org.mybatis.dynamic.sql.select.SelectDSLCompleter; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; @@ -28,10 +27,8 @@ public interface ColumnComparisonMapper { @SelectProvider(type=SqlProviderAdapter.class, method="select") - @Results({ - @Result(column="number1", property="number1", id=true), - @Result(column="number2", property="number2", id=true) - }) + @Result(column="number1", property="number1", id=true) + @Result(column="number2", property="number2", id=true) List selectMany(SelectStatementProvider selectStatement); default List select(SelectDSLCompleter completer) { diff --git a/src/test/java/examples/joins/JoinMapper.java b/src/test/java/examples/joins/JoinMapper.java index 447546035..0ce4cc42c 100644 --- a/src/test/java/examples/joins/JoinMapper.java +++ b/src/test/java/examples/joins/JoinMapper.java @@ -19,7 +19,6 @@ import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.ResultMap; -import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.SelectProvider; import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; import org.mybatis.dynamic.sql.util.SqlProviderAdapter; @@ -30,10 +29,8 @@ public interface JoinMapper { List selectMany(SelectStatementProvider selectStatement); @SelectProvider(type=SqlProviderAdapter.class, method="select") - @Results ({ - @Result(column="user_id", property="userId"), - @Result(column="user_name", property="userName"), - @Result(column="parent_id", property="parentId") - }) + @Result(column="user_id", property="userId") + @Result(column="user_name", property="userName") + @Result(column="parent_id", property="parentId") List selectUsers(SelectStatementProvider selectStatement); } diff --git a/src/test/java/examples/postgres/PostgresTest.java b/src/test/java/examples/postgres/PostgresTest.java new file mode 100644 index 000000000..f8da9c328 --- /dev/null +++ b/src/test/java/examples/postgres/PostgresTest.java @@ -0,0 +1,298 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.postgres; + +import static examples.postgres.TableCodeDynamicSqlSupport.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.List; +import java.util.Map; + +import config.TestContainersConfiguration; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class PostgresTest { + + @SuppressWarnings("resource") + @Container + private static final PostgreSQLContainer postgres = + new PostgreSQLContainer<>(TestContainersConfiguration.POSTGRES_LATEST) + .withInitScript("examples/postgres/dbInit.sql"); + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() { + UnpooledDataSource ds = new UnpooledDataSource(postgres.getDriverClassName(), postgres.getJdbcUrl(), + postgres.getUsername(), postgres.getPassword()); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration configuration = new Configuration(environment); + configuration.addMapper(CommonSelectMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); + } + + @Test + void testSelectForUpdate() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forUpdate() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for update"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForUpdateNoWait() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forUpdate() + .nowait() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for update nowait"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForUpdateSkipLocked() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forUpdate() + .skipLocked() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for update skip locked"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForNoKeyUpdate() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forNoKeyUpdate() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for no key update"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForNoKeyUpdateNoWait() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forNoKeyUpdate() + .nowait() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for no key update nowait"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForNoKeyUpdateSkipLocked() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forNoKeyUpdate() + .skipLocked() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for no key update skip locked"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForShare() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forShare() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for share"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForShareNoWait() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forShare() + .nowait() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for share nowait"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForShareSkipLocked() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forShare() + .skipLocked() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for share skip locked"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForKeyShare() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forKeyShare() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for key share"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForKeyShareNoWait() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forKeyShare() + .nowait() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for key share nowait"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } + + @Test + void testSelectForKeyShareSkipLocked() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = sqlSession.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(id, description) + .from(tableCode) + .orderBy(id) + .forKeyShare() + .skipLocked() + .build() + .render(RenderingStrategies.MYBATIS3); + + assertThat(selectStatement.getSelectStatement()) + .isEqualTo("select id, description from TableCode order by id for key share skip locked"); + List> records = mapper.selectManyMappedRows(selectStatement); + assertThat(records).hasSize(4); + } + } +} diff --git a/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java b/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java new file mode 100644 index 000000000..c2fd0deef --- /dev/null +++ b/src/test/java/examples/postgres/TableCodeDynamicSqlSupport.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package examples.postgres; + +import java.sql.JDBCType; + +import org.mybatis.dynamic.sql.SqlColumn; +import org.mybatis.dynamic.sql.SqlTable; + +public final class TableCodeDynamicSqlSupport { + public static final TableCode tableCode = new TableCode(); + public static final SqlColumn id = tableCode.id; + public static final SqlColumn description = tableCode.description; + + public static class TableCode extends SqlTable { + public final SqlColumn id = column("id", JDBCType.INTEGER); + public final SqlColumn description = column("description", JDBCType.VARCHAR); + + public TableCode() { + super("TableCode"); + } + } +} diff --git a/src/test/java/examples/springbatch/mapper/PersonMapper.java b/src/test/java/examples/springbatch/mapper/PersonMapper.java index 12d6837ee..a1db34626 100644 --- a/src/test/java/examples/springbatch/mapper/PersonMapper.java +++ b/src/test/java/examples/springbatch/mapper/PersonMapper.java @@ -20,7 +20,6 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Result; -import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.SelectProvider; import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper; import org.mybatis.dynamic.sql.util.mybatis3.CommonInsertMapper; @@ -33,10 +32,8 @@ public interface PersonMapper extends CommonCountMapper, CommonInsertMapper, CommonUpdateMapper { @SelectProvider(type=SpringBatchProviderAdapter.class, method="select") - @Results({ - @Result(column="id", property="id", id=true), - @Result(column="first_name", property="firstName"), - @Result(column="last_name", property="lastName") - }) + @Result(column="id", property="id", id=true) + @Result(column="first_name", property="firstName") + @Result(column="last_name", property="lastName") List selectMany(Map parameterValues); } diff --git a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java index 2464a4000..8e8981567 100644 --- a/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java +++ b/src/test/java/org/mybatis/dynamic/sql/InvalidSQLTest.java @@ -17,16 +17,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mybatis.dynamic.sql.SqlBuilder.insert; -import static org.mybatis.dynamic.sql.SqlBuilder.insertInto; -import static org.mybatis.dynamic.sql.SqlBuilder.update; -import static org.mybatis.dynamic.sql.SqlBuilder.value; +import static org.mybatis.dynamic.sql.SqlBuilder.*; import java.util.Collections; import java.util.List; import java.util.MissingResourceException; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mybatis.dynamic.sql.common.OrderByModel; import org.mybatis.dynamic.sql.configuration.StatementConfiguration; @@ -260,10 +258,59 @@ void testInvalidValueAlias() { .withMessage(Messages.getString("ERROR.38")); } + @Test + void testInvalidDoubleForUpdate() { + var dsl = select(id).from(person).limit(2).forUpdate(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forUpdate) + .withMessage(Messages.getString("ERROR.48")); + } + + @Test + void testInvalidDoubleForShare() { + var dsl = select(id).from(person).offset(2).forShare(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forShare) + .withMessage(Messages.getString("ERROR.48")); + } + + @Test + void testInvalidDoubleForKeyShare() { + var dsl = select(id).from(person).forKeyShare(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forKeyShare) + .withMessage(Messages.getString("ERROR.48")); + } + + @Test + void testInvalidDoubleForNoKeyUpdate() { + var dsl = select(id).from(person).where(id, isEqualTo(1)).forNoKeyUpdate(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::forNoKeyUpdate) + .withMessage(Messages.getString("ERROR.48")); + } + + @Test + void testInvalidDoubleForNoKeyUpdateAfterJoin() { + var dsl = select(id).from(person).join(person).on(id, isEqualTo(id)).skipLocked(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::skipLocked) + .withMessage(Messages.getString("ERROR.49")); + } + + @Test + void testInvalidDoubleForNoKeyUpdateAfterGroupBy() { + var dsl = select(id).from(person).groupBy(id).nowait(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::nowait) + .withMessage(Messages.getString("ERROR.49")); + } + + @Test + void testInvalidDoubleForNoKeyUpdateAfterHaving() { + var dsl = select(id).from(person).groupBy(id).having(id, isEqualTo(2)).nowait(); + assertThatExceptionOfType(InvalidSqlException.class).isThrownBy(dsl::nowait) + .withMessage(Messages.getString("ERROR.49")); + } + static class TestRow { - private Integer id; + private @Nullable Integer id; - public Integer getId() { + public @Nullable Integer getId() { return id; } diff --git a/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt b/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt index 878bad3b9..d4394a500 100644 --- a/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt +++ b/src/test/kotlin/org/mybatis/dynamic/sql/util/kotlin/model/ModelBuilderTest.kt @@ -46,4 +46,50 @@ class ModelBuilderTest { assertThat(provider.selectStatement).isEqualTo("select distinct id, description from Table where id = :p1") } + + @Test + fun testSelectBuilderForUpdate() { + val provider = select(id, description) { + from(table) + where { id isEqualTo 3 } + forUpdate() + skipLocked() + }.render(RenderingStrategies.SPRING_NAMED_PARAMETER) + + assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for update skip locked") + } + + @Test + fun testSelectBuilderForShare() { + val provider = select(id, description) { + from(table) + where { id isEqualTo 3 } + forShare() + nowait() + }.render(RenderingStrategies.SPRING_NAMED_PARAMETER) + + assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for share nowait") + } + + @Test + fun testSelectBuilderForKeyShare() { + val provider = select(id, description) { + from(table) + where { id isEqualTo 3 } + forKeyShare() + }.render(RenderingStrategies.SPRING_NAMED_PARAMETER) + + assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for key share") + } + + @Test + fun testSelectBuilderForKeyNoKeyUpdate() { + val provider = select(id, description) { + from(table) + where { id isEqualTo 3 } + forNoKeyUpdate() + }.render(RenderingStrategies.SPRING_NAMED_PARAMETER) + + assertThat(provider.selectStatement).isEqualTo("select id, description from Table where id = :p1 for no key update") + } } diff --git a/src/test/resources/examples/postgres/dbInit.sql b/src/test/resources/examples/postgres/dbInit.sql new file mode 100644 index 000000000..03ba34cac --- /dev/null +++ b/src/test/resources/examples/postgres/dbInit.sql @@ -0,0 +1,26 @@ +-- +-- Copyright 2016-2025 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +create table TableCode ( + id int not null, + description varchar(30) not null, + primary key (id) +); + +insert into TableCode (id, description) values(1, 'One'); +insert into TableCode (id, description) values(2, 'Two'); +insert into TableCode (id, description) values(3, 'Three'); +insert into TableCode (id, description) values(4, 'Four');