From 40d28e43701acd7ec904f1ee34e761a750dd3502 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 12 Aug 2024 20:34:47 -0400 Subject: [PATCH 1/9] Support Full Boolean Expressions in Joins --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 26 +- .../mybatis/dynamic/sql/delete/DeleteDSL.java | 4 +- .../select/AbstractQueryExpressionDSL.java | 134 +- .../sql/select/QueryExpressionDSL.java | 81 +- .../sql/select/join/JoinSpecification.java | 35 +- .../sql/select/render/JoinRenderer.java | 39 +- .../render/JoinSpecificationRenderer.java | 44 + .../mybatis/dynamic/sql/update/UpdateDSL.java | 4 +- .../sql/where/AbstractWhereStarter.java | 22 +- .../mybatis/dynamic/sql/where/WhereDSL.java | 2 +- .../dynamic/sql/util/kotlin/JoinCollector.kt | 25 +- .../joins/NewSyntaxJoinMapperTest.java | 1270 +++++++++++++++++ 12 files changed, 1480 insertions(+), 206 deletions(-) create mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java create mode 100644 src/test/java/examples/joins/NewSyntaxJoinMapperTest.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index df4292653..13f385e21 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -433,19 +433,9 @@ static AndOrCriteriaGroup and(List subCriteria) { } // join support - static JoinCriterion and(BindableColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); - } - - static JoinCriterion on(BindableColumn joinColumn, JoinCondition joinCondition) { - return new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + static ColumnAndConditionCriterion on(BindableColumn joinColumn, VisitableCondition joinCondition) { + return ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) .build(); } @@ -460,12 +450,14 @@ static SearchedCaseDSL case_() { return SearchedCaseDSL.searchedCase(); } - static EqualTo equalTo(BindableColumn column) { - return new EqualTo<>(column); + // TODO - Deprecate? + static IsEqualToColumn equalTo(BindableColumn column) { + return IsEqualToColumn.of(column); } - static EqualToValue equalTo(T value) { - return new EqualToValue<>(value); + // TODO - Deprecate? + static IsEqualTo equalTo(T value) { + return IsEqualTo.of(value); } // aggregate support diff --git a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java index e7c1ad184..0ee9a99c9 100644 --- a/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/delete/DeleteDSL.java @@ -32,8 +32,8 @@ import org.mybatis.dynamic.sql.where.AbstractWhereStarter; import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; -public class DeleteDSL extends AbstractWhereStarter.DeleteWhereBuilder, DeleteDSL> - implements Buildable { +public class DeleteDSL implements AbstractWhereStarter.DeleteWhereBuilder, DeleteDSL>, + Buildable { private final Function adapterFunction; private final SqlTable table; diff --git a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java index d82076a49..a1a44d2ab 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/AbstractQueryExpressionDSL.java @@ -23,11 +23,13 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; +import org.mybatis.dynamic.sql.SqlCriterion; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.TableExpression; import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; @@ -37,9 +39,9 @@ public abstract class AbstractQueryExpressionDSL, T extends AbstractQueryExpressionDSL> - extends AbstractWhereStarter { + implements AbstractWhereStarter { - private final List joinSpecificationBuilders = new ArrayList<>(); + private final List> joinSpecificationSuppliers = new ArrayList<>(); private final Map tableAliases = new HashMap<>(); private final TableExpression table; @@ -51,151 +53,151 @@ public TableExpression table() { return table; } - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); + public T join(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, Arrays.asList(andJoinCriteria)); return getThis(); } - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T join(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - public T join(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); + public T join(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.INNER, andJoinCriteria); return getThis(); } - public T join(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T join(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return join(joinTable, onJoinCriterion, andJoinCriteria); } - public T join(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER, + public T join(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.INNER, andJoinCriteria); return getThis(); } - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); + public T leftJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, Arrays.asList(andJoinCriteria)); return getThis(); } - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T leftJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T leftJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); + public T leftJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.LEFT, andJoinCriteria); return getThis(); } - public T leftJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T leftJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return leftJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T leftJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT, + public T leftJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.LEFT, andJoinCriteria); return getThis(); } - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); + public T rightJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, Arrays.asList(andJoinCriteria)); return getThis(); } - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T rightJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T rightJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); + public T rightJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.RIGHT, andJoinCriteria); return getThis(); } - public T rightJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T rightJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return rightJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T rightJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT, + public T rightJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.RIGHT, andJoinCriteria); return getThis(); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); + public T fullJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, Arrays.asList(andJoinCriteria)); return getThis(); } - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - JoinCriterion... andJoinCriteria) { + public T fullJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + AndOrCriteriaGroup... andJoinCriteria) { addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(SqlTable joinTable, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); + public T fullJoin(SqlTable joinTable, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(joinTable, onJoinCriterion, JoinType.FULL, andJoinCriteria); return getThis(); } - public T fullJoin(SqlTable joinTable, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { + public T fullJoin(SqlTable joinTable, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { addTableAlias(joinTable, tableAlias); return fullJoin(joinTable, onJoinCriterion, andJoinCriteria); } - public T fullJoin(Buildable subQuery, String tableAlias, JoinCriterion onJoinCriterion, - List> andJoinCriteria) { - addJoinSpecificationBuilder(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL, + public T fullJoin(Buildable subQuery, String tableAlias, SqlCriterion onJoinCriterion, + List andJoinCriteria) { + addJoinSpecificationSupplier(buildSubQuery(subQuery, tableAlias), onJoinCriterion, JoinType.FULL, andJoinCriteria); return getThis(); } - private void addJoinSpecificationBuilder(TableExpression joinTable, JoinCriterion onJoinCriterion, - JoinType joinType, List> andJoinCriteria) { - joinSpecificationBuilders.add(new JoinSpecification.Builder() + private void addJoinSpecificationSupplier(TableExpression joinTable, SqlCriterion onJoinCriterion, + JoinType joinType, List andJoinCriteria) { + joinSpecificationSuppliers.add(() -> new JoinSpecification.Builder() .withJoinTable(joinTable) .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(andJoinCriteria)); + .withInitialCriterion(onJoinCriterion) + .withSubCriteria(andJoinCriteria).build()); } - protected void addJoinSpecificationBuilder(JoinSpecification.Builder builder) { - joinSpecificationBuilders.add(builder); + protected void addJoinSpecificationSupplier(Supplier joinSpecificationSupplier) { + joinSpecificationSuppliers.add(joinSpecificationSupplier); } protected Optional buildJoinModel() { - if (joinSpecificationBuilders.isEmpty()) { + if (joinSpecificationSuppliers.isEmpty()) { return Optional.empty(); } - return Optional.of(JoinModel.of(joinSpecificationBuilders.stream() - .map(JoinSpecification.Builder::build) - .toList())); + return Optional.of(JoinModel.of(joinSpecificationSuppliers.stream() + .map(Supplier::get) + .toList())); } protected void addTableAlias(SqlTable table, String tableAlias) { 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 593252979..4c025b660 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -23,15 +23,17 @@ import java.util.function.Consumer; import org.jetbrains.annotations.NotNull; +import org.mybatis.dynamic.sql.AndOrCriteriaGroup; import org.mybatis.dynamic.sql.BasicColumn; import org.mybatis.dynamic.sql.BindableColumn; +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion; import org.mybatis.dynamic.sql.CriteriaGroup; import org.mybatis.dynamic.sql.SortSpecification; import org.mybatis.dynamic.sql.SqlTable; import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.VisitableCondition; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionDSL; import org.mybatis.dynamic.sql.configuration.StatementConfiguration; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.select.join.JoinType; import org.mybatis.dynamic.sql.util.Buildable; @@ -339,50 +341,54 @@ public JoinSpecificationStarter(TableExpression joinTable, JoinType joinType) { this.joinType = joinType; } - public JoinSpecificationFinisher on(BindableColumn joinColumn, JoinCondition joinCondition) { + public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition joinCondition) { return new JoinSpecificationFinisher(joinTable, joinColumn, joinCondition, joinType); } - public JoinSpecificationFinisher on(BindableColumn joinColumn, JoinCondition onJoinCondition, - JoinCriterion... andJoinCriteria) { - return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, andJoinCriteria); + public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableCondition onJoinCondition, + AndOrCriteriaGroup... subCriteria) { + return new JoinSpecificationFinisher(joinTable, joinColumn, onJoinCondition, joinType, subCriteria); } } - public class JoinSpecificationFinisher - extends AbstractWhereStarter - implements Buildable { - private final JoinSpecification.Builder joinSpecificationBuilder; + public class JoinSpecificationFinisher extends AbstractBooleanExpressionDSL + implements AbstractWhereStarter, Buildable { + private final TableExpression table; + private final JoinType joinType; public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - JoinCondition joinCondition, JoinType joinType) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); + VisitableCondition joinCondition, JoinType joinType) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(joinCriterion); + ColumnAndConditionCriterion c = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .build(); - addJoinSpecificationBuilder(joinSpecificationBuilder); + setInitialCriterion(c); } public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, - JoinCondition joinCondition, JoinType joinType, JoinCriterion... andJoinCriteria) { - JoinCriterion onJoinCriterion = new JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) + VisitableCondition joinCondition, JoinType joinType, AndOrCriteriaGroup... subCriteria) { + this.table = table; + this.joinType = joinType; + addJoinSpecificationSupplier(this::buildJoinSpecification); + + ColumnAndConditionCriterion c = ColumnAndConditionCriterion.withColumn(joinColumn) + .withCondition(joinCondition) + .withSubCriteria(Arrays.asList(subCriteria)) .build(); - joinSpecificationBuilder = JoinSpecification.withJoinTable(table) - .withJoinType(joinType) - .withJoinCriterion(onJoinCriterion) - .withJoinCriteria(Arrays.asList(andJoinCriteria)); + setInitialCriterion(c); + } - addJoinSpecificationBuilder(joinSpecificationBuilder); + private JoinSpecification buildJoinSpecification() { + return JoinSpecification.withJoinTable(table) + .withJoinType(joinType) + .withInitialCriterion(getInitialCriterion()) + .withSubCriteria(subCriteria) + .build(); } @NotNull @@ -402,16 +408,6 @@ public QueryExpressionWhereBuilder where() { return QueryExpressionDSL.this.where(); } - public JoinSpecificationFinisher and(BindableColumn joinColumn, JoinCondition joinCondition) { - JoinCriterion joinCriterion = new JoinCriterion.Builder() - .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(joinColumn) - .withJoinCondition(joinCondition) - .build(); - joinSpecificationBuilder.withJoinCriterion(joinCriterion); - return this; - } - public JoinSpecificationStarter join(SqlTable joinTable) { return QueryExpressionDSL.this.join(joinTable); } @@ -495,6 +491,11 @@ public SelectDSL.OffsetFirstFinisher offset(long offset) { public SelectDSL.FetchFirstFinisher fetchFirst(long fetchFirstRows) { return QueryExpressionDSL.this.fetchFirst(fetchFirstRows); } + + @Override + protected JoinSpecificationFinisher getThis() { + return this; + } } public class GroupByFinisher extends AbstractHavingStarter implements Buildable { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java index ab95dcf5e..9db5243d2 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinSpecification.java @@ -15,36 +15,29 @@ */ package org.mybatis.dynamic.sql.select.join; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; -import java.util.stream.Stream; import org.mybatis.dynamic.sql.TableExpression; +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionModel; import org.mybatis.dynamic.sql.util.Validator; -public class JoinSpecification { +public class JoinSpecification extends AbstractBooleanExpressionModel { private final TableExpression table; - private final List> joinCriteria; private final JoinType joinType; private JoinSpecification(Builder builder) { + super(builder); table = Objects.requireNonNull(builder.table); - joinCriteria = Objects.requireNonNull(builder.joinCriteria); joinType = Objects.requireNonNull(builder.joinType); - Validator.assertNotEmpty(joinCriteria, "ERROR.16"); //$NON-NLS-1$ + Validator.assertFalse(initialCriterion().isEmpty() && subCriteria().isEmpty(), + "ERROR.16"); //$NON-NLS-1$ } public TableExpression table() { return table; } - @SuppressWarnings("java:S1452") - public Stream> joinCriteria() { - return joinCriteria.stream(); - } - public JoinType joinType() { return joinType; } @@ -53,9 +46,8 @@ public static Builder withJoinTable(TableExpression table) { return new Builder().withJoinTable(table); } - public static class Builder { + public static class Builder extends AbstractBuilder { private TableExpression table; - private final List> joinCriteria = new ArrayList<>(); private JoinType joinType; public Builder withJoinTable(TableExpression table) { @@ -63,16 +55,6 @@ public Builder withJoinTable(TableExpression table) { return this; } - public Builder withJoinCriterion(JoinCriterion joinCriterion) { - this.joinCriteria.add(joinCriterion); - return this; - } - - public Builder withJoinCriteria(List> joinCriteria) { - this.joinCriteria.addAll(joinCriteria); - return this; - } - public Builder withJoinType(JoinType joinType) { this.joinType = joinType; return this; @@ -81,5 +63,10 @@ public Builder withJoinType(JoinType joinType) { public JoinSpecification build() { return new JoinSpecification(this); } + + @Override + protected Builder getThis() { + return this; + } } } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java index 667b4e299..0da029f59 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java @@ -20,8 +20,8 @@ import java.util.Objects; import java.util.stream.Collectors; +import org.mybatis.dynamic.sql.exception.InvalidSqlException; import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.select.join.JoinModel; import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.util.FragmentAndParameters; @@ -47,41 +47,20 @@ public FragmentAndParameters render() { private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) { FragmentAndParameters renderedTable = joinSpecification.table().accept(tableExpressionRenderer); - FragmentAndParameters renderedJoin = renderConditions(joinSpecification); + FragmentAndParameters renderedJoinSpecification = JoinSpecificationRenderer + .withJoinSpecification(joinSpecification) + .withRenderingContext(renderingContext) + .build() + .render() + .orElseThrow(() -> new InvalidSqlException("Join Specifications Must Render")); // TODO String fragment = joinSpecification.joinType().type() + spaceBefore(renderedTable.fragment()) - + spaceBefore(renderedJoin.fragment()); + + spaceBefore(renderedJoinSpecification.fragment()); return FragmentAndParameters.withFragment(fragment) .withParameters(renderedTable.parameters()) - .withParameters(renderedJoin.parameters()) - .build(); - } - - private FragmentAndParameters renderConditions(JoinSpecification joinSpecification) { - return joinSpecification.joinCriteria() - .map(this::renderCriterion) - .collect(FragmentCollector.collect()) - .toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ - } - - private FragmentAndParameters renderCriterion(JoinCriterion joinCriterion) { - FragmentAndParameters renderedColumn = joinCriterion.leftColumn().render(renderingContext); - - String prefix = joinCriterion.connector() - + spaceBefore(renderedColumn.fragment()); - - JoinConditionRenderer joinConditionRenderer = new JoinConditionRenderer.Builder() - .withRenderingContext(renderingContext) - .withLeftColumn(joinCriterion.leftColumn()) - .build(); - - FragmentAndParameters suffix = joinCriterion.joinCondition().accept(joinConditionRenderer); - - return FragmentAndParameters.withFragment(prefix + spaceBefore(suffix.fragment())) - .withParameters(suffix.parameters()) - .withParameters(renderedColumn.parameters()) + .withParameters(renderedJoinSpecification.parameters()) .build(); } diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java new file mode 100644 index 000000000..300e416d8 --- /dev/null +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinSpecificationRenderer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2024 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.render; + +import org.mybatis.dynamic.sql.common.AbstractBooleanExpressionRenderer; +import org.mybatis.dynamic.sql.select.join.JoinSpecification; + +public class JoinSpecificationRenderer extends AbstractBooleanExpressionRenderer { + private JoinSpecificationRenderer(Builder builder) { + super("on", builder); //$NON-NLS-1$ + } + + public static JoinSpecificationRenderer.Builder withJoinSpecification(JoinSpecification joinSpecification) { + return new Builder(joinSpecification); + } + + public static class Builder extends AbstractBuilder { + public Builder(JoinSpecification joinSpecification) { + super(joinSpecification); + } + + public JoinSpecificationRenderer build() { + return new JoinSpecificationRenderer(this); + } + + @Override + protected Builder getThis() { + return this; + } + } +} diff --git a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java index 8cf7259a3..4daa99ccc 100644 --- a/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/update/UpdateDSL.java @@ -47,8 +47,8 @@ import org.mybatis.dynamic.sql.where.AbstractWhereStarter; import org.mybatis.dynamic.sql.where.EmbeddedWhereModel; -public class UpdateDSL extends AbstractWhereStarter.UpdateWhereBuilder, UpdateDSL> - implements Buildable { +public class UpdateDSL implements AbstractWhereStarter.UpdateWhereBuilder, UpdateDSL>, + Buildable { private final Function adapterFunction; private final List columnMappings = new ArrayList<>(); diff --git a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java index 03da51bd8..e94f0a8a1 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/AbstractWhereStarter.java @@ -35,14 +35,14 @@ * * @param the implementation of the Where DSL customized for a particular SQL statement. */ -public abstract class AbstractWhereStarter, D extends AbstractWhereStarter> - implements ConfigurableStatement { +public interface AbstractWhereStarter, D extends AbstractWhereStarter> + extends ConfigurableStatement { - public F where(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { + default F where(BindableColumn column, VisitableCondition condition, AndOrCriteriaGroup... subCriteria) { return where(column, condition, Arrays.asList(subCriteria)); } - public F where(BindableColumn column, VisitableCondition condition, + default F where(BindableColumn column, VisitableCondition condition, List subCriteria) { SqlCriterion sqlCriterion = ColumnAndConditionCriterion.withColumn(column) .withCondition(condition) @@ -52,11 +52,11 @@ public F where(BindableColumn column, VisitableCondition condition, return initialize(sqlCriterion); } - public F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { + default F where(ExistsPredicate existsPredicate, AndOrCriteriaGroup... subCriteria) { return where(existsPredicate, Arrays.asList(subCriteria)); } - public F where(ExistsPredicate existsPredicate, List subCriteria) { + default F where(ExistsPredicate existsPredicate, List subCriteria) { ExistsCriterion sqlCriterion = new ExistsCriterion.Builder() .withExistsPredicate(existsPredicate) .withSubCriteria(subCriteria) @@ -65,11 +65,11 @@ public F where(ExistsPredicate existsPredicate, List subCrit return initialize(sqlCriterion); } - public F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { + default F where(SqlCriterion initialCriterion, AndOrCriteriaGroup... subCriteria) { return where(initialCriterion, Arrays.asList(subCriteria)); } - public F where(SqlCriterion initialCriterion, List subCriteria) { + default F where(SqlCriterion initialCriterion, List subCriteria) { SqlCriterion sqlCriterion = new CriteriaGroup.Builder() .withInitialCriterion(initialCriterion) .withSubCriteria(subCriteria) @@ -78,7 +78,7 @@ public F where(SqlCriterion initialCriterion, List subCriter return initialize(sqlCriterion); } - public F where(List subCriteria) { + default F where(List subCriteria) { SqlCriterion sqlCriterion = new CriteriaGroup.Builder() .withSubCriteria(subCriteria) .build(); @@ -86,9 +86,9 @@ public F where(List subCriteria) { return initialize(sqlCriterion); } - public abstract F where(); + F where(); - public F applyWhere(WhereApplier whereApplier) { + default F applyWhere(WhereApplier whereApplier) { F finisher = where(); whereApplier.accept(finisher); return finisher; diff --git a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java index 0eb23f638..a4adc2967 100644 --- a/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/where/WhereDSL.java @@ -26,7 +26,7 @@ * *

This can also be used to create reusable where clauses for different statements. */ -public class WhereDSL extends AbstractWhereStarter { +public class WhereDSL implements AbstractWhereStarter { private final StatementConfiguration statementConfiguration = new StatementConfiguration(); private final StandaloneWhereFinisher whereBuilder = new StandaloneWhereFinisher(); diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 3ba5c2acb..6212167c1 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -15,40 +15,39 @@ */ package org.mybatis.dynamic.sql.util.kotlin +import org.mybatis.dynamic.sql.AndOrCriteriaGroup import org.mybatis.dynamic.sql.BindableColumn +import org.mybatis.dynamic.sql.ColumnAndConditionCriterion import org.mybatis.dynamic.sql.SqlBuilder -import org.mybatis.dynamic.sql.select.join.JoinCondition -import org.mybatis.dynamic.sql.select.join.JoinCriterion +import org.mybatis.dynamic.sql.SqlCriterion +import org.mybatis.dynamic.sql.VisitableCondition typealias JoinReceiver = JoinCollector.() -> Unit @MyBatisDslMarker class JoinCollector { - private var onJoinCriterion: JoinCriterion<*>? = null - internal val andJoinCriteria = mutableListOf>() + private var onJoinCriterion: SqlCriterion? = null + internal val andJoinCriteria = mutableListOf() - internal fun onJoinCriterion() : JoinCriterion<*> = invalidIfNull(onJoinCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun onJoinCriterion() : SqlCriterion = invalidIfNull(onJoinCriterion, "ERROR.22") //$NON-NLS-1$ fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - onJoinCriterion = JoinCriterion.Builder() - .withConnector("on") //$NON-NLS-1$ - .withJoinColumn(leftColumn) - .withJoinCondition(it) + onJoinCriterion = ColumnAndConditionCriterion.withColumn(leftColumn) + .withCondition(it) .build() } fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { andJoinCriteria.add( - JoinCriterion.Builder() + AndOrCriteriaGroup.Builder() .withConnector("and") //$NON-NLS-1$ - .withJoinColumn(leftColumn) - .withJoinCondition(it) + .withInitialCriterion(ColumnAndConditionCriterion.withColumn(leftColumn).withCondition(it).build()) .build() ) } } -class RightColumnCollector(private val joinConditionConsumer: (JoinCondition) -> Unit) { +class RightColumnCollector(private val joinConditionConsumer: (VisitableCondition) -> Unit) { infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.equalTo(rightColumn)) infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.equalTo(value)) diff --git a/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java b/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java new file mode 100644 index 000000000..ae8f5d0f0 --- /dev/null +++ b/src/test/java/examples/joins/NewSyntaxJoinMapperTest.java @@ -0,0 +1,1270 @@ +/* + * Copyright 2016-2024 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.joins; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.jdbc.ScriptRunner; +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.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.dynamic.sql.exception.DuplicateTableAliasException; +import org.mybatis.dynamic.sql.render.RenderingStrategies; +import org.mybatis.dynamic.sql.select.QueryExpressionDSL; +import org.mybatis.dynamic.sql.select.SelectModel; +import org.mybatis.dynamic.sql.select.render.SelectStatementProvider; +import org.mybatis.dynamic.sql.util.Messages; +import org.mybatis.dynamic.sql.util.mybatis3.CommonSelectMapper; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.List; +import java.util.Map; + +import static examples.joins.ItemMasterDynamicSQLSupport.itemMaster; +import static examples.joins.OrderDetailDynamicSQLSupport.orderDetail; +import static examples.joins.OrderLineDynamicSQLSupport.orderLine; +import static examples.joins.OrderMasterDynamicSQLSupport.orderDate; +import static examples.joins.OrderMasterDynamicSQLSupport.orderMaster; +import static examples.joins.UserDynamicSQLSupport.user; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mybatis.dynamic.sql.SqlBuilder.and; +import static org.mybatis.dynamic.sql.SqlBuilder.constant; +import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.on; +import static org.mybatis.dynamic.sql.SqlBuilder.select; +import static org.mybatis.dynamic.sql.SqlBuilder.sortColumn; +import static org.mybatis.dynamic.sql.SqlBuilder.where; + +class NewSyntaxJoinMapperTest { + + private static final String JDBC_URL = "jdbc:hsqldb:mem:aname"; + private static final String JDBC_DRIVER = "org.hsqldb.jdbcDriver"; + + private SqlSessionFactory sqlSessionFactory; + + @BeforeEach + void setup() throws Exception { + Class.forName(JDBC_DRIVER); + InputStream is = getClass().getResourceAsStream("/examples/joins/CreateJoinDB.sql"); + assert is != null; + try (Connection connection = DriverManager.getConnection(JDBC_URL, "sa", "")) { + ScriptRunner sr = new ScriptRunner(connection); + sr.setLogWriter(null); + sr.runScript(new InputStreamReader(is)); + } + + UnpooledDataSource ds = new UnpooledDataSource(JDBC_DRIVER, JDBC_URL, "sa", ""); + Environment environment = new Environment("test", new JdbcTransactionFactory(), ds); + Configuration config = new Configuration(environment); + config.addMapper(JoinMapper.class); + config.addMapper(CommonSelectMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(config); + } + + @Test + void testSingleTableJoin1() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + } + } + + @Test + void testSingleTableJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + } + } + + @Test + void testCompoundJoin1() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin2() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .configureStatement(c -> c.setNonRenderingWhereClauseAllowed(true)) + .and(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin3() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin4() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .leftJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om left join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin5() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .rightJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om right join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testCompoundJoin6() { + // this is a nonsensical join, but it does test the "and" capability + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .fullJoin(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId)), and(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om full join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + } + + @Test + void testMultipleTableJoinWithWhereClause() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithApplyWhere() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .applyWhere(where(orderMaster.orderId, isEqualTo(2)).toWhereApplier()) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithComplexWhereClause() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2), and(orderLine.lineNumber, isEqualTo(2))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER} and ol.line_number = #{parameters.p2,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(1); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinWithOrderBy() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderMaster.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(2); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(1); + assertThat(orderMaster.getDetails()).hasSize(1); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + + orderMaster = rows.get(1); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testMultipleTableJoinNoAliasWithOrderBy() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .orderBy(orderMaster.orderId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderMaster.order_id, OrderMaster.order_date, OrderLine.line_number, ItemMaster.description, OrderLine.quantity" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " where OrderMaster.order_id = #{parameters.p1,jdbcType=INTEGER}" + + " order by order_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectMany(selectStatement); + + assertThat(rows).hasSize(1); + OrderMaster orderMaster = rows.get(0); + assertThat(orderMaster.getId()).isEqualTo(2); + assertThat(orderMaster.getDetails()).hasSize(2); + OrderDetail orderDetail = orderMaster.getDetails().get(0); + assertThat(orderDetail.getLineNumber()).isEqualTo(1); + orderDetail = orderMaster.getDetails().get(1); + assertThat(orderDetail.getLineNumber()).isEqualTo(2); + } + } + + @Test + void testRightJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderLine, "ol") + .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderLine ol right join ItemMaster im on ol.item_id = im.item_id" + + " order by item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(4); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + } + } + + @Test + void testRightJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .rightJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testRightJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .rightJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testRightJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .rightJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " right join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " order by item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(4); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + } + } + + @Test + void testLeftJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .leftJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .leftJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testLeftJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .leftJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " left join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(5); + Map row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(4); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, orderLine.itemId.as("ol_itemid"), itemMaster.itemId.as("im_itemid"), itemMaster.description) + .from(itemMaster, "im") + .fullJoin(orderLine, "ol").on(itemMaster.itemId, isEqualTo(orderLine.itemId)) + .orderBy(orderLine.orderId, sortColumn("im_itemid")) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, ol.item_id as ol_itemid, im.item_id as im_itemid, im.description" + + " from ItemMaster im full join OrderLine ol on im.item_id = ol.item_id" + + " order by order_id, im_itemid"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("IM_ITEMID", 55); + + row = rows.get(2); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("IM_ITEMID", 33); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).containsEntry("OL_ITEMID", 66); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("IM_ITEMID"); + } + } + + @Test + void testFullJoin2() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .fullJoin(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin3() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testFullJoin4() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, im.item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + } + } + + @Test + void testFullJoin5() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.description) + .from(orderMaster, "om") + .join(orderLine, "ol", on(orderMaster.orderId, isEqualTo(orderLine.orderId))) + .fullJoin(itemMaster, "im", on(orderLine.itemId, isEqualTo(itemMaster.itemId))) + .orderBy(orderLine.orderId, sortColumn("im", itemMaster.itemId).descending()) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, im.item_id DESC"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + } + } + + @Test + void testFullJoinNoAliases() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(orderMaster) + .join(orderLine).on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .fullJoin(itemMaster).on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .orderBy(orderLine.orderId, itemMaster.itemId) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select OrderLine.order_id, OrderLine.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster join OrderLine on OrderMaster.order_id = OrderLine.order_id" + + " full join ItemMaster on OrderLine.item_id = ItemMaster.item_id" + + " order by order_id, item_id"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(6); + Map row = rows.get(0); + assertThat(row).doesNotContainKey("ORDER_ID"); + assertThat(row).doesNotContainKey("QUANTITY"); + assertThat(row).containsEntry("DESCRIPTION", "Catcher Glove"); + assertThat(row).containsEntry("ITEM_ID", 55); + + row = rows.get(3); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 6); + assertThat(row).doesNotContainKey("DESCRIPTION"); + assertThat(row).doesNotContainKey("ITEM_ID"); + + row = rows.get(5); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testSelf() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = new UserDynamicSQLSupport.User(); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user, "u1") + .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testSelfWithDuplicateAlias() { + QueryExpressionDSL dsl = select(user.userId, user.userName, user.parentId) + .from(user, "u1"); + + assertThatExceptionOfType(DuplicateTableAliasException.class).isThrownBy(() -> dsl.join(user, "u2")) + .withMessage(Messages.getString("ERROR.1", user.tableName(), "u2", "u1")); + } + + @Test + void testSelfWithNewAlias() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = user.withAlias("u2"); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user) + .join(user2).on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select User.user_id, User.user_name, User.parent_id" + + " from User join User u2 on User.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testSelfWithNewAliasAndOverride() { + try (SqlSession session = sqlSessionFactory.openSession()) { + JoinMapper mapper = session.getMapper(JoinMapper.class); + + // create second table instance for self-join + UserDynamicSQLSupport.User user2 = user.withAlias("other_user"); + + // get Bamm Bamm's parent - should be Barney + SelectStatementProvider selectStatement = select(user.userId, user.userName, user.parentId) + .from(user, "u1") + .join(user2, "u2").on(user.userId, isEqualTo(user2.parentId)) + .where(user2.userId, isEqualTo(4)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List rows = mapper.selectUsers(selectStatement); + + assertThat(rows).hasSize(1); + User row = rows.get(0); + assertThat(row.getUserId()).isEqualTo(2); + assertThat(row.getUserName()).isEqualTo("Barney"); + assertThat(row.getParentId()).isNull(); + } + } + + @Test + void testLimitAndOffsetAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .limit(2) + .offset(1) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " limit #{parameters.p1} offset #{parameters.p2}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testLimitOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .limit(2) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " limit #{parameters.p1}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + } + } + + @Test + void testOffsetOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .offset(2) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " offset #{parameters.p1} rows"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(3); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Outfield Glove"); + assertThat(row).containsEntry("ITEM_ID", 44); + } + } + + @Test + void testOffsetAndFetchFirstAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .offset(1) + .fetchFirst(2).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " offset #{parameters.p1} rows fetch first #{parameters.p2} rows only"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testFetchFirstOnlyAfterJoin() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .leftJoin(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .fetchFirst(2).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im left join OrderLine ol on ol.item_id = im.item_id" + + " fetch first #{parameters.p1} rows only"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 2); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + } + } + + @Test + void testJoinWithParameterValue() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .and(orderLine.orderId, isEqualTo(1)) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im join OrderLine ol on ol.item_id = im.item_id" + + " and ol.order_id = #{parameters.p1,jdbcType=INTEGER}"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } + + @Test + void testJoinWithConstant() { + try (SqlSession session = sqlSessionFactory.openSession()) { + CommonSelectMapper mapper = session.getMapper(CommonSelectMapper.class); + + SelectStatementProvider selectStatement = select(orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description) + .from(itemMaster, "im") + .join(orderLine, "ol").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .and(orderLine.orderId, isEqualTo(constant("1"))) + .build() + .render(RenderingStrategies.MYBATIS3); + + String expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from ItemMaster im join OrderLine ol on ol.item_id = im.item_id" + + " and ol.order_id = 1"; + assertThat(selectStatement.getSelectStatement()).isEqualTo(expectedStatement); + + List> rows = mapper.selectManyMappedRows(selectStatement); + + assertThat(rows).hasSize(2); + Map row = rows.get(0); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "Helmet"); + assertThat(row).containsEntry("ITEM_ID", 22); + + row = rows.get(1); + assertThat(row).containsEntry("ORDER_ID", 1); + assertThat(row).containsEntry("QUANTITY", 1); + assertThat(row).containsEntry("DESCRIPTION", "First Base Glove"); + assertThat(row).containsEntry("ITEM_ID", 33); + } + } +} From 6816dacbe6097495adfd044d8f3cd71607a026a2 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 12 Aug 2024 20:46:31 -0400 Subject: [PATCH 2/9] Remove obsolete code --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 4 - .../sql/select/QueryExpressionDSL.java | 8 +- .../select/join/ColumnBasedJoinCondition.java | 37 ---------- .../dynamic/sql/select/join/EqualTo.java | 30 -------- .../dynamic/sql/select/join/EqualToValue.java | 27 ------- .../sql/select/join/JoinCondition.java | 22 ------ .../sql/select/join/JoinConditionVisitor.java | 22 ------ .../sql/select/join/JoinCriterion.java | 70 ------------------ .../sql/select/join/TypedJoinCondition.java | 35 --------- .../select/render/JoinConditionRenderer.java | 73 ------------------- 10 files changed, 4 insertions(+), 324 deletions(-) delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java delete mode 100644 src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 13f385e21..88871f6f0 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -56,10 +56,6 @@ import org.mybatis.dynamic.sql.select.function.Substring; import org.mybatis.dynamic.sql.select.function.Subtract; import org.mybatis.dynamic.sql.select.function.Upper; -import org.mybatis.dynamic.sql.select.join.EqualTo; -import org.mybatis.dynamic.sql.select.join.EqualToValue; -import org.mybatis.dynamic.sql.select.join.JoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinCriterion; import org.mybatis.dynamic.sql.update.UpdateDSL; import org.mybatis.dynamic.sql.update.UpdateModel; import org.mybatis.dynamic.sql.util.Buildable; 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 4c025b660..be1121c85 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -362,11 +362,11 @@ public JoinSpecificationFinisher(TableExpression table, BindableColumn jo this.joinType = joinType; addJoinSpecificationSupplier(this::buildJoinSpecification); - ColumnAndConditionCriterion c = ColumnAndConditionCriterion.withColumn(joinColumn) + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) .withCondition(joinCondition) .build(); - setInitialCriterion(c); + setInitialCriterion(criterion); } public JoinSpecificationFinisher(TableExpression table, BindableColumn joinColumn, @@ -375,12 +375,12 @@ public JoinSpecificationFinisher(TableExpression table, BindableColumn jo this.joinType = joinType; addJoinSpecificationSupplier(this::buildJoinSpecification); - ColumnAndConditionCriterion c = ColumnAndConditionCriterion.withColumn(joinColumn) + ColumnAndConditionCriterion criterion = ColumnAndConditionCriterion.withColumn(joinColumn) .withCondition(joinCondition) .withSubCriteria(Arrays.asList(subCriteria)) .build(); - setInitialCriterion(c); + setInitialCriterion(criterion); } private JoinSpecification buildJoinSpecification() { diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java deleted file mode 100644 index 712010ca6..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/ColumnBasedJoinCondition.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BasicColumn; - -public abstract class ColumnBasedJoinCondition implements JoinCondition { - private final BasicColumn rightColumn; - - protected ColumnBasedJoinCondition(BasicColumn rightColumn) { - this.rightColumn = Objects.requireNonNull(rightColumn); - } - - public BasicColumn rightColumn() { - return rightColumn; - } - - @Override - public R accept(JoinConditionVisitor visitor) { - return visitor.visit(this); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java b/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java deleted file mode 100644 index 6f12eb052..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualTo.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -import org.mybatis.dynamic.sql.BasicColumn; - -public class EqualTo extends ColumnBasedJoinCondition { - - public EqualTo(BasicColumn rightColumn) { - super(rightColumn); - } - - @Override - public String operator() { - return "="; //$NON-NLS-1$ - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java b/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java deleted file mode 100644 index 2d038aa21..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/EqualToValue.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -public class EqualToValue extends TypedJoinCondition { - public EqualToValue(T value) { - super(value); - } - - @Override - public String operator() { - return "="; //$NON-NLS-1$ - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java deleted file mode 100644 index 183bd9a02..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCondition.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -public interface JoinCondition { - String operator(); - - R accept(JoinConditionVisitor visitor); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java deleted file mode 100644 index 582b332f1..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinConditionVisitor.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -public interface JoinConditionVisitor { - R visit(TypedJoinCondition condition); - - R visit(ColumnBasedJoinCondition condition); -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java b/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java deleted file mode 100644 index 81925f0e4..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/JoinCriterion.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BindableColumn; - -public class JoinCriterion { - - private final String connector; - private final BindableColumn leftColumn; - private final JoinCondition joinCondition; - - private JoinCriterion(Builder builder) { - connector = Objects.requireNonNull(builder.connector); - leftColumn = Objects.requireNonNull(builder.joinColumn); - joinCondition = Objects.requireNonNull(builder.joinCondition); - } - - public String connector() { - return connector; - } - - public BindableColumn leftColumn() { - return leftColumn; - } - - public JoinCondition joinCondition() { - return joinCondition; - } - - public static class Builder { - private String connector; - private BindableColumn joinColumn; - private JoinCondition joinCondition; - - public Builder withConnector(String connector) { - this.connector = connector; - return this; - } - - public Builder withJoinColumn(BindableColumn joinColumn) { - this.joinColumn = joinColumn; - return this; - } - - public Builder withJoinCondition(JoinCondition joinCondition) { - this.joinCondition = joinCondition; - return this; - } - - public JoinCriterion build() { - return new JoinCriterion<>(this); - } - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java b/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java deleted file mode 100644 index 12b310d1f..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/join/TypedJoinCondition.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016-2024 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.join; - -import java.util.Objects; - -public abstract class TypedJoinCondition implements JoinCondition { - private final T value; - - protected TypedJoinCondition(T value) { - this.value = Objects.requireNonNull(value); - } - - public T value() { - return value; - } - - @Override - public R accept(JoinConditionVisitor visitor) { - return visitor.visit(this); - } -} diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java deleted file mode 100644 index 2cba1a951..000000000 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinConditionRenderer.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016-2024 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.render; - -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - -import java.util.Objects; - -import org.mybatis.dynamic.sql.BindableColumn; -import org.mybatis.dynamic.sql.render.RenderedParameterInfo; -import org.mybatis.dynamic.sql.render.RenderingContext; -import org.mybatis.dynamic.sql.select.join.ColumnBasedJoinCondition; -import org.mybatis.dynamic.sql.select.join.JoinConditionVisitor; -import org.mybatis.dynamic.sql.select.join.TypedJoinCondition; -import org.mybatis.dynamic.sql.util.FragmentAndParameters; - -public class JoinConditionRenderer implements JoinConditionVisitor { - private final BindableColumn leftColumn; - private final RenderingContext renderingContext; - - private JoinConditionRenderer(Builder builder) { - leftColumn = Objects.requireNonNull(builder.leftColumn); - renderingContext = Objects.requireNonNull(builder.renderingContext); - } - - @Override - public FragmentAndParameters visit(TypedJoinCondition condition) { - RenderedParameterInfo parameterInfo = renderingContext.calculateParameterInfo(leftColumn); - - return FragmentAndParameters - .withFragment(condition.operator() + spaceBefore(parameterInfo.renderedPlaceHolder())) - .withParameter(parameterInfo.parameterMapKey(), condition.value()) - .build(); - } - - @Override - public FragmentAndParameters visit(ColumnBasedJoinCondition condition) { - return condition.rightColumn().render(renderingContext) - .mapFragment(s -> condition.operator() + spaceBefore(s)); - } - - public static class Builder { - private BindableColumn leftColumn; - private RenderingContext renderingContext; - - public Builder withLeftColumn(BindableColumn leftColumn) { - this.leftColumn = leftColumn; - return this; - } - - public Builder withRenderingContext(RenderingContext renderingContext) { - this.renderingContext = renderingContext; - return this; - } - - public JoinConditionRenderer build() { - return new JoinConditionRenderer<>(this); - } - } -} From 3cc83da9613e8c6fb04044ba91ac888c90ed42be Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Tue, 13 Aug 2024 20:55:43 -0400 Subject: [PATCH 3/9] Update Kotlin DSL for new Join syntax --- .../dynamic/sql/util/kotlin/JoinCollector.kt | 19 +- .../sql/util/kotlin/KotlinBaseBuilders.kt | 24 +- .../mybatis3/joins/JoinMapperNewSyntaxTest.kt | 818 ++++++++++++++++++ 3 files changed, 844 insertions(+), 17 deletions(-) create mode 100644 src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 6212167c1..0cb43d1d4 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -26,19 +26,28 @@ typealias JoinReceiver = JoinCollector.() -> Unit @MyBatisDslMarker class JoinCollector { - private var onJoinCriterion: SqlCriterion? = null - internal val andJoinCriteria = mutableListOf() + private var initialCriterion: SqlCriterion? = null + internal val subCriteria = mutableListOf() - internal fun onJoinCriterion() : SqlCriterion = invalidIfNull(onJoinCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun initialCriterion() : SqlCriterion = invalidIfNull(initialCriterion, "ERROR.22") //$NON-NLS-1$ + fun on (receiver: GroupingCriteriaReceiver) { + GroupingCriteriaCollector().apply(receiver).also { + initialCriterion = it.initialCriterion + subCriteria.addAll(it.subCriteria) + } + } + + // TODO - Deprecate? fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - onJoinCriterion = ColumnAndConditionCriterion.withColumn(leftColumn) + initialCriterion = ColumnAndConditionCriterion.withColumn(leftColumn) .withCondition(it) .build() } + // TODO - Deprecate? fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - andJoinCriteria.add( + subCriteria.add( AndOrCriteriaGroup.Builder() .withConnector("and") //$NON-NLS-1$ .withInitialCriterion(ColumnAndConditionCriterion.withColumn(leftColumn).withCondition(it).build()) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index 4207905c6..e9d070320 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -66,12 +66,12 @@ abstract class KotlinBaseJoiningBuilder> : fun join(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, jc.onJoinCriterion(), jc.andJoinCriteria) + join(table, jc.initialCriterion(), jc.subCriteria) } fun join(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + join(table, alias, jc.initialCriterion(), jc.subCriteria) } fun join( @@ -79,17 +79,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - join(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) } fun fullJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(table, jc.initialCriterion(), jc.subCriteria) } fun fullJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria) } fun fullJoin( @@ -97,17 +97,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - fullJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) } fun leftJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(table, jc.initialCriterion(), jc.subCriteria) } fun leftJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria) } fun leftJoin( @@ -115,17 +115,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - leftJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) } fun rightJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(table, jc.initialCriterion(), jc.subCriteria) } fun rightJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, alias, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria) } fun rightJoin( @@ -133,7 +133,7 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - rightJoin(sq, sq.correlationName, jc.onJoinCriterion(), jc.andJoinCriteria) + rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) } private fun applyToDsl(joinCriteria: JoinReceiver, applyJoin: D.(JoinCollector) -> Unit) { diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt new file mode 100644 index 000000000..a99124a12 --- /dev/null +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt @@ -0,0 +1,818 @@ +/* + * Copyright 2016-2024 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.kotlin.mybatis3.joins + +import examples.kotlin.mybatis3.TestUtils +import examples.kotlin.mybatis3.joins.ItemMasterDynamicSQLSupport.itemMaster +import examples.kotlin.mybatis3.joins.OrderDetailDynamicSQLSupport.orderDetail +import examples.kotlin.mybatis3.joins.OrderLineDynamicSQLSupport.orderLine +import examples.kotlin.mybatis3.joins.OrderMasterDynamicSQLSupport.orderMaster +import examples.kotlin.mybatis3.joins.UserDynamicSQLSupport.user +import org.apache.ibatis.session.SqlSessionFactory +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.assertj.core.api.Assertions.entry +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.mybatis.dynamic.sql.util.Messages +import org.mybatis.dynamic.sql.util.kotlin.KInvalidSQLException +import org.mybatis.dynamic.sql.util.kotlin.elements.constant +import org.mybatis.dynamic.sql.util.kotlin.elements.invoke +import org.mybatis.dynamic.sql.util.kotlin.mybatis3.select + +@Suppress("LargeClass") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class JoinMapperNewSyntaxTest { + private lateinit var sqlSessionFactory: SqlSessionFactory + + @BeforeAll + fun setup() { + sqlSessionFactory = TestUtils.buildSqlSessionFactory { + withInitializationScript("/examples/kotlin/mybatis3/joins/CreateJoinDB.sql") + withMapper(JoinMapper::class) + } + } + + @Test + fun testSingleTableJoin() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") { + on { orderMaster.orderId isEqualTo orderDetail.orderId } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(2) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + + with(rows[1]) { + assertThat(id).isEqualTo(2) + assertThat(details).hasSize(1) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + } + } + } + + @Test + fun testSingleTableJoinWithValue() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") { + on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo 1 } + } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + " and om.order_id = #{parameters.p1,jdbcType=INTEGER}" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testSingleTableJoinWithConstant() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") { + on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } + } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id" + + " and om.order_id = 1" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + + with(rows[0]) { + assertThat(id).isEqualTo(1) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testCompoundJoin1() { + // this is a nonsensical join, but it does test the "and" capability + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderDetail.lineNumber, + orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") { + on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } + } + } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + } + + @Test + fun testCompoundJoin2() { + // this is a nonsensical join, but it does test the "and" capability + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderDetail.lineNumber, + orderDetail.description, orderDetail.quantity + ) { + from(orderMaster, "om") + join(orderDetail, "od") { + on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } + } + } + where { orderMaster.orderId isEqualTo 1 } + } + + val expectedStatement = "select om.order_id, om.order_date, od.line_number, od.description, od.quantity" + + " from OrderMaster om join OrderDetail od on om.order_id = od.order_id and om.order_id = od.order_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + } + + @Test + fun testMultipleTableJoinWithWhereClause() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, orderLine.lineNumber, + itemMaster.description, orderLine.quantity + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + join(itemMaster, "im") { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + where { orderMaster.orderId isEqualTo 2 } + } + + val expectedStatement = "select om.order_id, om.order_date, ol.line_number, im.description, ol.quantity" + + " from OrderMaster om join OrderLine ol" + + " on om.order_id = ol.order_id join ItemMaster im on ol.item_id = im.item_id" + + " where om.order_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectMany(selectStatement) + + assertThat(rows).hasSize(1) + with(rows[0]) { + assertThat(id).isEqualTo(2) + assertThat(details).hasSize(2) + assertThat(details?.get(0)?.lineNumber).isEqualTo(1) + assertThat(details?.get(1)?.lineNumber).isEqualTo(2) + } + } + } + + @Test + fun testFullJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + fullJoin(itemMaster, "im") { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?) + + val rows = mapper.selectMany(selectStatement) { + OrderDetail( + it["ITEM_ID"] as Int?, + it["ORDER_ID"] as Int?, + it["QUANTITY"] as Int?, + it["DESCRIPTION"] as String? + ) + } + + assertThat(rows).hasSize(6) + + with(rows[0]) { + assertThat(itemId).isEqualTo(55) + assertThat(orderId).isNull() + assertThat(quantity).isNull() + assertThat(description).isEqualTo("Catcher Glove") + } + + with(rows[3]) { + assertThat(itemId).isNull() + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(6) + assertThat(description).isNull() + } + + with(rows[5]) { + assertThat(itemId).isEqualTo(44) + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(1) + assertThat(description).isEqualTo("Outfield Glove") + } + } + } + + @Test + @Suppress("LongMethod") + fun testFullJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + "ol"(orderLine.orderId), orderLine.quantity, "im"(itemMaster.itemId), + itemMaster.description + ) { + from { + select(orderMaster.allColumns()) { + from(orderMaster) + } + + "om" + } + join( + subQuery = { + select(orderLine.allColumns()) { + from(orderLine) + } + + "ol" + }, + joinCriteria = { + on { "om"(orderMaster.orderId) isEqualTo "ol"(orderLine.orderId) } + } + ) + fullJoin( + { + select(itemMaster.allColumns()) { + from(itemMaster) + } + + "im" + } + ) { + on { "ol"(orderLine.itemId) isEqualTo "im"(itemMaster.itemId) } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, quantity, im.item_id, description" + + " from (select * from OrderMaster) om" + + " join (select * from OrderLine) ol on om.order_id = ol.order_id" + + " full join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + data class OrderDetail(val itemId: Int?, val orderId: Int?, val quantity: Int?, val description: String?) + + val rows = mapper.selectMany(selectStatement) { + OrderDetail( + it["ITEM_ID"] as Int?, + it["ORDER_ID"] as Int?, + it["QUANTITY"] as Int?, + it["DESCRIPTION"] as String? + ) + } + + assertThat(rows).hasSize(6) + + with(rows[0]) { + assertThat(itemId).isEqualTo(55) + assertThat(orderId).isNull() + assertThat(quantity).isNull() + assertThat(description).isEqualTo("Catcher Glove") + } + + with(rows[3]) { + assertThat(itemId).isNull() + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(6) + assertThat(description).isNull() + } + + with(rows[5]) { + assertThat(itemId).isEqualTo(44) + assertThat(orderId).isEqualTo(2) + assertThat(quantity).isEqualTo(1) + assertThat(description).isEqualTo("Outfield Glove") + } + } + } + + @Test + fun testFullJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + fullJoin(itemMaster) { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " full join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(6) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[3]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[5]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + leftJoin(itemMaster, "im") { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, "im"(itemMaster.itemId), + itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + leftJoin( + { + select(itemMaster.allColumns()) { + from(itemMaster) + } + + "im" + } + ) { + on { orderLine.itemId isEqualTo "im"(itemMaster.itemId) } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testLeftJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + leftJoin(itemMaster) { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " left join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[2]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 6) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + rightJoin(itemMaster, "im") { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, im.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithSubQuery() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, + "im"(itemMaster.itemId), itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + rightJoin( + { + select(itemMaster.allColumns()) { + from(itemMaster) + } + + "im" + } + ) { + on { orderLine.itemId isEqualTo "im"(itemMaster.itemId) } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, im.item_id, description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join (select * from ItemMaster) im on ol.item_id = im.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testRightJoinWithoutAliases() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + val selectStatement = select( + orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description + ) { + from(orderMaster, "om") + join(orderLine, "ol") { + on { orderMaster.orderId isEqualTo orderLine.orderId } + } + rightJoin(itemMaster) { + on { orderLine.itemId isEqualTo itemMaster.itemId } + } + orderBy(orderLine.orderId, itemMaster.itemId) + } + + val expectedStatement = "select ol.order_id, ol.quantity, ItemMaster.item_id, ItemMaster.description" + + " from OrderMaster om join OrderLine ol on om.order_id = ol.order_id" + + " right join ItemMaster on ol.item_id = ItemMaster.item_id" + + " order by order_id, item_id" + + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(5) + + assertThat(rows[0]).containsExactly( + entry("DESCRIPTION", "Catcher Glove"), + entry("ITEM_ID", 55) + ) + + assertThat(rows[4]).containsExactly( + entry("ORDER_ID", 2), + entry("QUANTITY", 1), + entry("DESCRIPTION", "Outfield Glove"), + entry("ITEM_ID", 44) + ) + } + } + + @Test + fun testSelf() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = UserDynamicSQLSupport.User() + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { + on { user.userId isEqualTo user2.parentId } + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + + assertThat(rows).hasSize(1) + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testSelfWithNewAlias() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = user.withAlias("u2") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user) + join(user2) { + on { user.userId isEqualTo user2.parentId } + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select User.user_id, User.user_name, User.parent_id" + + " from User join User u2 on User.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testSelfWithNewAliasAndOverride() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) + + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { + on { user.userId isEqualTo user2.parentId } + } + where { user2.userId isEqualTo 4 } + } + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } + } + + @Test + fun testJoinWithNoOnCondition() { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { } + where { user2.userId isEqualTo 4 } + } + }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ + } +} From 52d897a8c537c8c79cd3dd2cb3a19c851cc192f7 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Sat, 17 Aug 2024 07:45:14 -0400 Subject: [PATCH 4/9] Add some kotlin error checking --- .../dynamic/sql/util/kotlin/JoinCollector.kt | 27 ++++++------------- .../sql/util/kotlin/KotlinBaseBuilders.kt | 24 ++++++++--------- .../dynamic/sql/util/messages.properties | 1 + .../mybatis3/joins/JoinMapperNewSyntaxTest.kt | 17 ++++++++++++ .../kotlin/mybatis3/joins/JoinMapperTest.kt | 17 ++++++++++++ 5 files changed, 55 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index 0cb43d1d4..ec63179db 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -15,44 +15,33 @@ */ package org.mybatis.dynamic.sql.util.kotlin -import org.mybatis.dynamic.sql.AndOrCriteriaGroup import org.mybatis.dynamic.sql.BindableColumn -import org.mybatis.dynamic.sql.ColumnAndConditionCriterion import org.mybatis.dynamic.sql.SqlBuilder -import org.mybatis.dynamic.sql.SqlCriterion import org.mybatis.dynamic.sql.VisitableCondition typealias JoinReceiver = JoinCollector.() -> Unit @MyBatisDslMarker class JoinCollector { - private var initialCriterion: SqlCriterion? = null - internal val subCriteria = mutableListOf() + private val criteriaCollector = GroupingCriteriaCollector() - internal fun initialCriterion() : SqlCriterion = invalidIfNull(initialCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun initialCriterion() = invalidIfNull(criteriaCollector.initialCriterion, "ERROR.22") //$NON-NLS-1$ + internal fun subCriteria() = criteriaCollector.subCriteria fun on (receiver: GroupingCriteriaReceiver) { - GroupingCriteriaCollector().apply(receiver).also { - initialCriterion = it.initialCriterion - subCriteria.addAll(it.subCriteria) - } + assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ + criteriaCollector.apply(receiver) } // TODO - Deprecate? fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - initialCriterion = ColumnAndConditionCriterion.withColumn(leftColumn) - .withCondition(it) - .build() + assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ + criteriaCollector.apply { leftColumn.invoke(it) } } // TODO - Deprecate? fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { - subCriteria.add( - AndOrCriteriaGroup.Builder() - .withConnector("and") //$NON-NLS-1$ - .withInitialCriterion(ColumnAndConditionCriterion.withColumn(leftColumn).withCondition(it).build()) - .build() - ) + criteriaCollector.and { leftColumn.invoke(it) } } } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index e9d070320..88386b2c1 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -66,12 +66,12 @@ abstract class KotlinBaseJoiningBuilder> : fun join(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, jc.initialCriterion(), jc.subCriteria) + join(table, jc.initialCriterion(), jc.subCriteria()) } fun join(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - join(table, alias, jc.initialCriterion(), jc.subCriteria) + join(table, alias, jc.initialCriterion(), jc.subCriteria()) } fun join( @@ -79,17 +79,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) + join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } fun fullJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, jc.initialCriterion(), jc.subCriteria) + fullJoin(table, jc.initialCriterion(), jc.subCriteria()) } fun fullJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria) + fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } fun fullJoin( @@ -97,17 +97,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) + fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } fun leftJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, jc.initialCriterion(), jc.subCriteria) + leftJoin(table, jc.initialCriterion(), jc.subCriteria()) } fun leftJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria) + leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } fun leftJoin( @@ -115,17 +115,17 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) + leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } fun rightJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, jc.initialCriterion(), jc.subCriteria) + rightJoin(table, jc.initialCriterion(), jc.subCriteria()) } fun rightJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> - rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria) + rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } fun rightJoin( @@ -133,7 +133,7 @@ abstract class KotlinBaseJoiningBuilder> : joinCriteria: JoinReceiver ): Unit = applyToDsl(subQuery, joinCriteria) { sq, jc -> - rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria) + rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } private fun applyToDsl(joinCriteria: JoinReceiver, applyJoin: D.(JoinCollector) -> Unit) { 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 ef092e651..8600b0437 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -61,4 +61,5 @@ ERROR.41=You cannot call "then" in a Kotlin case expression more than once ERROR.42=You cannot call `else` in a Kotlin case expression more than once ERROR.43=A Kotlin cast expression must have one, and only one, `as` element ERROR.44={0} conditions must contain at least one value +ERROR.45=You cannot call "on" in a Kotlin join expression more than once INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt index a99124a12..cc2159c13 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt @@ -815,4 +815,21 @@ class JoinMapperNewSyntaxTest { } }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ } + + @Test + fun testJoinWithDoubleOnCondition() { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { + on { user.userId isEqualTo user2.parentId } + on { user.userId isEqualTo user2.parentId } + } + where { user2.userId isEqualTo 4 } + } + }.withMessage(Messages.getString("ERROR.45")) //$NON-NLS-1$ + } } diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt index a60bc2105..5754fe631 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperTest.kt @@ -813,6 +813,23 @@ class JoinMapperTest { }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ } + @Test + fun testJoinWithDoubleOnCondition() { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { + select(user.userId, user.userName, user.parentId) { + from(user, "u1") + join(user2, "u2") { + on(user.userId) equalTo user2.parentId + on(user.userId) equalTo user2.parentId + } + where { user2.userId isEqualTo 4 } + } + }.withMessage(Messages.getString("ERROR.45")) //$NON-NLS-1$ + } + @Test fun testThatAliasesPropagateToSubQueryConditions() { sqlSessionFactory.openSession().use { session -> From 3cd872b2db21c05eac25526a62141d98879c2ec3 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 Aug 2024 11:02:46 -0400 Subject: [PATCH 5/9] Add deprecation notices --- .../org/mybatis/dynamic/sql/SqlBuilder.java | 36 +++++++++++++------ .../dynamic/sql/util/kotlin/JoinCollector.kt | 8 ++--- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 88871f6f0..4468b523a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -435,6 +435,32 @@ static ColumnAndConditionCriterion on(BindableColumn joinColumn, Visit .build(); } + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(BasicColumn)}. + * + * @param column the column + * @return an IsEqualToColumn condition + * @param the column type + * @deprecated since 2.0.0. Please replace with isEqualTo(column) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualToColumn equalTo(BindableColumn column) { + return isEqualTo(column); + } + + /** + * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(Object)}. + * + * @param value the value + * @return an IsEqualTo condition + * @param the column type + * @deprecated since 2.0.0. Please replace with isEqualTo(value) + */ + @Deprecated(since = "2.0.0", forRemoval = true) + static IsEqualTo equalTo(T value) { + return isEqualTo(value); + } + // case expressions @SuppressWarnings("java:S100") static SimpleCaseDSL case_(BindableColumn column) { @@ -446,16 +472,6 @@ static SearchedCaseDSL case_() { return SearchedCaseDSL.searchedCase(); } - // TODO - Deprecate? - static IsEqualToColumn equalTo(BindableColumn column) { - return IsEqualToColumn.of(column); - } - - // TODO - Deprecate? - static IsEqualTo equalTo(T value) { - return IsEqualTo.of(value); - } - // aggregate support static CountAll count() { return new CountAll(); diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index ec63179db..ba68b4a8e 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -33,20 +33,20 @@ class JoinCollector { criteriaCollector.apply(receiver) } - // TODO - Deprecate? + @Deprecated("Please replace with the \"on\" lambda expression", level = DeprecationLevel.WARNING) fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ criteriaCollector.apply { leftColumn.invoke(it) } } - // TODO - Deprecate? + @Deprecated("Please move the \"and\" expression into an \"on\" lambda", level = DeprecationLevel.WARNING) fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { criteriaCollector.and { leftColumn.invoke(it) } } } class RightColumnCollector(private val joinConditionConsumer: (VisitableCondition) -> Unit) { - infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.equalTo(rightColumn)) + infix fun equalTo(rightColumn: BindableColumn) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(rightColumn)) - infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.equalTo(value)) + infix fun equalTo(value: T) = joinConditionConsumer.invoke(SqlBuilder.isEqualTo(value)) } From f888b831b4a6fb1e8fe4bf1ef8bb2b7d29c493d9 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 Aug 2024 15:13:29 -0400 Subject: [PATCH 6/9] New join syntax for Kotlin --- .../sql/select/render/JoinRenderer.java | 20 +- .../dynamic/sql/util/kotlin/JoinCollector.kt | 7 - .../sql/util/kotlin/KotlinBaseBuilders.kt | 88 ++++++++ .../dynamic/sql/util/messages.properties | 1 + .../mybatis3/joins/JoinMapperNewSyntaxTest.kt | 191 +++++++++--------- 5 files changed, 192 insertions(+), 115 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java index 0da029f59..6a437f010 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/render/JoinRenderer.java @@ -15,8 +15,6 @@ */ package org.mybatis.dynamic.sql.select.render; -import static org.mybatis.dynamic.sql.util.StringUtilities.spaceBefore; - import java.util.Objects; import java.util.stream.Collectors; @@ -26,6 +24,7 @@ import org.mybatis.dynamic.sql.select.join.JoinSpecification; import org.mybatis.dynamic.sql.util.FragmentAndParameters; import org.mybatis.dynamic.sql.util.FragmentCollector; +import org.mybatis.dynamic.sql.util.Messages; public class JoinRenderer { private final JoinModel joinModel; @@ -46,22 +45,17 @@ public FragmentAndParameters render() { } private FragmentAndParameters renderJoinSpecification(JoinSpecification joinSpecification) { - FragmentAndParameters renderedTable = joinSpecification.table().accept(tableExpressionRenderer); - FragmentAndParameters renderedJoinSpecification = JoinSpecificationRenderer + FragmentCollector fc = new FragmentCollector(); + fc.add(FragmentAndParameters.fromFragment(joinSpecification.joinType().type())); + fc.add(joinSpecification.table().accept(tableExpressionRenderer)); + fc.add(JoinSpecificationRenderer .withJoinSpecification(joinSpecification) .withRenderingContext(renderingContext) .build() .render() - .orElseThrow(() -> new InvalidSqlException("Join Specifications Must Render")); // TODO - - String fragment = joinSpecification.joinType().type() - + spaceBefore(renderedTable.fragment()) - + spaceBefore(renderedJoinSpecification.fragment()); + .orElseThrow(() -> new InvalidSqlException(Messages.getString("ERROR.46")))); //$NON-NLS-1$ - return FragmentAndParameters.withFragment(fragment) - .withParameters(renderedTable.parameters()) - .withParameters(renderedJoinSpecification.parameters()) - .build(); + return fc.toFragmentAndParameters(Collectors.joining(" ")); //$NON-NLS-1$ } public static Builder withJoinModel(JoinModel joinModel) { diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt index ba68b4a8e..078ea2a34 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/JoinCollector.kt @@ -28,18 +28,11 @@ class JoinCollector { internal fun initialCriterion() = invalidIfNull(criteriaCollector.initialCriterion, "ERROR.22") //$NON-NLS-1$ internal fun subCriteria() = criteriaCollector.subCriteria - fun on (receiver: GroupingCriteriaReceiver) { - assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ - criteriaCollector.apply(receiver) - } - - @Deprecated("Please replace with the \"on\" lambda expression", level = DeprecationLevel.WARNING) fun on(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { assertNull(criteriaCollector.initialCriterion, "ERROR.45") //$NON-NLS-1$ criteriaCollector.apply { leftColumn.invoke(it) } } - @Deprecated("Please move the \"and\" expression into an \"on\" lambda", level = DeprecationLevel.WARNING) fun and(leftColumn: BindableColumn): RightColumnCollector = RightColumnCollector { criteriaCollector.and { leftColumn.invoke(it) } } diff --git a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt index 88386b2c1..059792b2c 100644 --- a/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt +++ b/src/main/kotlin/org/mybatis/dynamic/sql/util/kotlin/KotlinBaseBuilders.kt @@ -64,16 +64,19 @@ abstract class KotlinBaseBuilder> { @Suppress("TooManyFunctions") abstract class KotlinBaseJoiningBuilder> : KotlinBaseBuilder() { + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> join(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> join(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun join( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver @@ -82,16 +85,36 @@ abstract class KotlinBaseJoiningBuilder> : join(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } + fun join(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, it.initialCriterion, it.subCriteria) + } + + fun join(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().join(table, alias, it.initialCriterion, it.subCriteria) + } + + fun join( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().join(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> fullJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> fullJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun fullJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver @@ -100,16 +123,36 @@ abstract class KotlinBaseJoiningBuilder> : fullJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } + fun fullJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, it.initialCriterion, it.subCriteria) + } + + fun fullJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().fullJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun fullJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().fullJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> leftJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> leftJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun leftJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver @@ -118,16 +161,36 @@ abstract class KotlinBaseJoiningBuilder> : leftJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } + fun leftJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, it.initialCriterion, it.subCriteria) + } + + fun leftJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().leftJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun leftJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().leftJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin(table: SqlTable, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> rightJoin(table, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin(table: SqlTable, alias: String, joinCriteria: JoinReceiver): Unit = applyToDsl(joinCriteria) { jc -> rightJoin(table, alias, jc.initialCriterion(), jc.subCriteria()) } + @Deprecated("Please use the new form with the \"on\" keyword outside the lambda") fun rightJoin( subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit, joinCriteria: JoinReceiver @@ -136,6 +199,23 @@ abstract class KotlinBaseJoiningBuilder> : rightJoin(sq, sq.correlationName, jc.initialCriterion(), jc.subCriteria()) } + fun rightJoin(table: SqlTable): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, it.initialCriterion, it.subCriteria) + } + + fun rightJoin(table: SqlTable, alias: String): JoinCriteriaGatherer = + JoinCriteriaGatherer { + getDsl().rightJoin(table, alias, it.initialCriterion, it.subCriteria) + } + + fun rightJoin( + subQuery: KotlinQualifiedSubQueryBuilder.() -> Unit): JoinCriteriaGatherer = + JoinCriteriaGatherer { + val sq = KotlinQualifiedSubQueryBuilder().apply(subQuery) + getDsl().rightJoin(sq, sq.correlationName, it.initialCriterion, it.subCriteria) + } + private fun applyToDsl(joinCriteria: JoinReceiver, applyJoin: D.(JoinCollector) -> Unit) { getDsl().applyJoin(JoinCollector().apply(joinCriteria)) } @@ -148,3 +228,11 @@ abstract class KotlinBaseJoiningBuilder> : getDsl().applyJoin(KotlinQualifiedSubQueryBuilder().apply(subQuery), JoinCollector().apply(joinCriteria)) } } + +class JoinCriteriaGatherer(private val consumer: (GroupingCriteriaCollector) -> Unit) { + infix fun on (joinCriteria: GroupingCriteriaReceiver): Unit = + with(GroupingCriteriaCollector().apply(joinCriteria)) { + assertTrue(initialCriterion != null || subCriteria.isNotEmpty(), "ERROR.22") //$NON-NLS-1$ + consumer.invoke(this) + } +} 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 8600b0437..05ecfcfe8 100644 --- a/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties +++ b/src/main/resources/org/mybatis/dynamic/sql/util/messages.properties @@ -62,4 +62,5 @@ ERROR.42=You cannot call `else` in a Kotlin case expression more than once ERROR.43=A Kotlin cast expression must have one, and only one, `as` element 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 INTERNAL.ERROR=Internal Error {0} diff --git a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt index cc2159c13..1b69ba5ec 100644 --- a/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt +++ b/src/test/kotlin/examples/kotlin/mybatis3/joins/JoinMapperNewSyntaxTest.kt @@ -57,8 +57,8 @@ class JoinMapperNewSyntaxTest { orderDetail.lineNumber, orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on { orderMaster.orderId isEqualTo orderDetail.orderId } + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId } } @@ -96,11 +96,9 @@ class JoinMapperNewSyntaxTest { orderDetail.lineNumber, orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on { - orderMaster.orderId isEqualTo orderDetail.orderId - and { orderMaster.orderId isEqualTo 1 } - } + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo 1 } } } @@ -133,11 +131,9 @@ class JoinMapperNewSyntaxTest { orderDetail.lineNumber, orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on { - orderMaster.orderId isEqualTo orderDetail.orderId - and { orderMaster.orderId isEqualTo constant("1") } - } + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } } } @@ -168,11 +164,9 @@ class JoinMapperNewSyntaxTest { orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on { - orderMaster.orderId isEqualTo orderDetail.orderId - and { orderMaster.orderId isEqualTo orderDetail.orderId } - } + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } } } @@ -189,11 +183,9 @@ class JoinMapperNewSyntaxTest { orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on { - orderMaster.orderId isEqualTo orderDetail.orderId - and { orderMaster.orderId isEqualTo orderDetail.orderId } - } + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } } where { orderMaster.orderId isEqualTo 1 } } @@ -214,11 +206,11 @@ class JoinMapperNewSyntaxTest { itemMaster.description, orderLine.quantity ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - join(itemMaster, "im") { - on { orderLine.itemId isEqualTo itemMaster.itemId } + join(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId } where { orderMaster.orderId isEqualTo 2 } } @@ -250,11 +242,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - fullJoin(itemMaster, "im") { - on { orderLine.itemId isEqualTo itemMaster.itemId } + fullJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -318,26 +310,21 @@ class JoinMapperNewSyntaxTest { } + "om" } - join( - subQuery = { - select(orderLine.allColumns()) { - from(orderLine) - } - + "ol" - }, - joinCriteria = { - on { "om"(orderMaster.orderId) isEqualTo "ol"(orderLine.orderId) } + join { + select(orderLine.allColumns()) { + from(orderLine) } - ) - fullJoin( - { - select(itemMaster.allColumns()) { - from(itemMaster) - } - + "im" + + "ol" + } on { + "om"(orderMaster.orderId) isEqualTo "ol"(orderLine.orderId) + } + fullJoin { + select(itemMaster.allColumns()) { + from(itemMaster) } - ) { - on { "ol"(orderLine.itemId) isEqualTo "im"(itemMaster.itemId) } + +"im" + } on { + "ol"(orderLine.itemId) isEqualTo "im"(itemMaster.itemId) } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -395,11 +382,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - fullJoin(itemMaster) { - on { orderLine.itemId isEqualTo itemMaster.itemId } + fullJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -443,11 +430,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - leftJoin(itemMaster, "im") { - on { orderLine.itemId isEqualTo itemMaster.itemId } + leftJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -487,8 +474,8 @@ class JoinMapperNewSyntaxTest { itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } leftJoin( { @@ -497,8 +484,8 @@ class JoinMapperNewSyntaxTest { } + "im" } - ) { - on { orderLine.itemId isEqualTo "im"(itemMaster.itemId) } + ) on { + orderLine.itemId isEqualTo "im"(itemMaster.itemId) } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -537,11 +524,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - leftJoin(itemMaster) { - on { orderLine.itemId isEqualTo itemMaster.itemId } + leftJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -580,11 +567,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - rightJoin(itemMaster, "im") { - on { orderLine.itemId isEqualTo itemMaster.itemId } + rightJoin(itemMaster, "im") on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -624,8 +611,8 @@ class JoinMapperNewSyntaxTest { "im"(itemMaster.itemId), itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } rightJoin( { @@ -634,8 +621,8 @@ class JoinMapperNewSyntaxTest { } + "im" } - ) { - on { orderLine.itemId isEqualTo "im"(itemMaster.itemId) } + ) on { + orderLine.itemId isEqualTo "im"(itemMaster.itemId) } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -674,11 +661,11 @@ class JoinMapperNewSyntaxTest { orderLine.orderId, orderLine.quantity, itemMaster.itemId, itemMaster.description ) { from(orderMaster, "om") - join(orderLine, "ol") { - on { orderMaster.orderId isEqualTo orderLine.orderId } + join(orderLine, "ol") on { + orderMaster.orderId isEqualTo orderLine.orderId } - rightJoin(itemMaster) { - on { orderLine.itemId isEqualTo itemMaster.itemId } + rightJoin(itemMaster) on { + orderLine.itemId isEqualTo itemMaster.itemId } orderBy(orderLine.orderId, itemMaster.itemId) } @@ -719,8 +706,8 @@ class JoinMapperNewSyntaxTest { // get Bamm Bamm's parent - should be Barney val selectStatement = select(user.userId, user.userName, user.parentId) { from(user, "u1") - join(user2, "u2") { - on { user.userId isEqualTo user2.parentId } + join(user2, "u2") on { + user.userId isEqualTo user2.parentId } where { user2.userId isEqualTo 4 } } @@ -750,8 +737,8 @@ class JoinMapperNewSyntaxTest { // get Bamm Bamm's parent - should be Barney val selectStatement = select(user.userId, user.userName, user.parentId) { from(user) - join(user2) { - on { user.userId isEqualTo user2.parentId } + join(user2) on { + user.userId isEqualTo user2.parentId } where { user2.userId isEqualTo 4 } } @@ -782,8 +769,8 @@ class JoinMapperNewSyntaxTest { // get Bamm Bamm's parent - should be Barney val selectStatement = select(user.userId, user.userName, user.parentId) { from(user, "u1") - join(user2, "u2") { - on { user.userId isEqualTo user2.parentId } + join(user2, "u2") on { + user.userId isEqualTo user2.parentId } where { user2.userId isEqualTo 4 } } @@ -803,33 +790,47 @@ class JoinMapperNewSyntaxTest { } @Test - fun testJoinWithNoOnCondition() { - // create second table instance for self-join - val user2 = user.withAlias("other_user") + fun testSelfWithNewAliasAndOverrideOddUsage() { + sqlSessionFactory.openSession().use { session -> + val mapper = session.getMapper(JoinMapper::class.java) - assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { - select(user.userId, user.userName, user.parentId) { + // create second table instance for self-join + val user2 = user.withAlias("other_user") + + // get Bamm Bamm's parent - should be Barney + val selectStatement = select(user.userId, user.userName, user.parentId) { from(user, "u1") - join(user2, "u2") { } + join(user2, "u2") on { + and { user.userId isEqualTo user2.parentId } + } where { user2.userId isEqualTo 4 } } - }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ + + val expectedStatement = "select u1.user_id, u1.user_name, u1.parent_id" + + " from User u1 join User u2 on u1.user_id = u2.parent_id" + + " where u2.user_id = #{parameters.p1,jdbcType=INTEGER}" + assertThat(selectStatement.selectStatement).isEqualTo(expectedStatement) + val rows = mapper.selectManyMappedRows(selectStatement) + assertThat(rows).hasSize(1) + + assertThat(rows[0]).containsExactly( + entry("USER_ID", 2), + entry("USER_NAME", "Barney"), + ) + } } @Test - fun testJoinWithDoubleOnCondition() { + fun testJoinWithNoOnCondition() { // create second table instance for self-join val user2 = user.withAlias("other_user") assertThatExceptionOfType(KInvalidSQLException::class.java).isThrownBy { select(user.userId, user.userName, user.parentId) { from(user, "u1") - join(user2, "u2") { - on { user.userId isEqualTo user2.parentId } - on { user.userId isEqualTo user2.parentId } - } + join(user2, "u2") on { } where { user2.userId isEqualTo 4 } } - }.withMessage(Messages.getString("ERROR.45")) //$NON-NLS-1$ + }.withMessage(Messages.getString("ERROR.22")) //$NON-NLS-1$ } } From 4984924f2462e677a998320f21f17bffe0d8dd9b Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 Aug 2024 15:51:10 -0400 Subject: [PATCH 7/9] Docs --- CHANGELOG.md | 4 +- src/site/markdown/docs/kotlinOverview.md | 9 +- src/site/markdown/docs/migratingV1toV2.md | 69 +++++++++ src/site/markdown/docs/select.md | 168 ++++++++++++---------- src/site/site.xml | 1 + 5 files changed, 168 insertions(+), 83 deletions(-) create mode 100644 src/site/markdown/docs/migratingV1toV2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c7526c2..2d46635fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,9 @@ Other important changes: With this change, the exception thrown is more predictable and the error is caught before sending the SQL to the database. - All the paging methods (limit, offset, fetchFirst) now have "WhenPresent" variations that will drop the phrase from - rendering if a null value is passed in + 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. ## Release 1.5.2 - June 3, 2024 diff --git a/src/site/markdown/docs/kotlinOverview.md b/src/site/markdown/docs/kotlinOverview.md index 5b646a7b0..78dba6a78 100644 --- a/src/site/markdown/docs/kotlinOverview.md +++ b/src/site/markdown/docs/kotlinOverview.md @@ -417,9 +417,9 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe orderDetail.description, orderDetail.quantity ) { from(orderMaster, "om") - join(orderDetail, "od") { - on(orderMaster.orderId) equalTo orderDetail.orderId - and(orderMaster.orderId) equalTo orderDetail.orderId + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo orderDetail.orderId } } where { orderMaster.orderId isEqualTo 1 } or { @@ -433,8 +433,7 @@ val selectStatement = select(orderMaster.orderId, orderMaster.orderDate, orderDe In a select statement you must specify a table in a `from` clause. Everything else is optional. -Multiple join clauses can be specified if you need to join additional tables. In a join clause, you must -specify an `on` condition, and you may specify additional `and` conditions as necessary. Full, left, right, inner, +Multiple join clauses can be specified if you need to join additional tables. Full, left, right, inner, and outer joins are supported. Where clauses can be of arbitrary complexity and support all SQL operators including exists operators, subqueries, etc. diff --git a/src/site/markdown/docs/migratingV1toV2.md b/src/site/markdown/docs/migratingV1toV2.md new file mode 100644 index 000000000..8d51e68f9 --- /dev/null +++ b/src/site/markdown/docs/migratingV1toV2.md @@ -0,0 +1,69 @@ +# V1 to V2 Migration Guide + +Version 2 of MyBatis Dynamic SQL introduced many new features. This page will document how to migrate code +from prior releases to version 2. + +## Java Join Syntax + +Version2 offers a fully flexible join specification and reuses the capabilities of where clauses. Of course, +not all capabilities are supported in databases, but you should now be able to code any type of join specification. + +The changes in the Java DSL are mostly internal and should not impact most users. The `equalTo` methods has been +deprecated in favor of `isEqualTo`, but all other changes should be hidden. + +V1 Join Specification Example: +```java +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDetail.lineNumber, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +V2 Join Specification Example: +```java +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDetail.lineNumber, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId))) + .build() + .render(RenderingStrategies.MYBATIS3); +``` + +## Kotlin Join Syntax + +Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and reuses the capabilities of where +clauses. Of course, not all capabilities are supported in databases, but you should now be able to code any type of +join specification. + +The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main difference is that +the "on" keyword should be moved outside the join specification lambda (it is now an infix function). Inside the lambda, +the conditions should be rewritten to match the syntax of a where clause. + +V1 Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") { + on(orderMaster.orderId) equalTo orderDetail.orderId + and(orderMaster.orderId) equalTo constant("1") + } +} +``` + +V2 Join Specification Example: +```kotlin +val selectStatement = select( + orderMaster.orderId, orderMaster.orderDate, + orderDetail.lineNumber, orderDetail.description, orderDetail.quantity +) { + from(orderMaster, "om") + join(orderDetail, "od") on { + orderMaster.orderId isEqualTo orderDetail.orderId + and { orderMaster.orderId isEqualTo constant("1") } + } +} + +``` diff --git a/src/site/markdown/docs/select.md b/src/site/markdown/docs/select.md index 923c30c42..f73ef17dd 100644 --- a/src/site/markdown/docs/select.md +++ b/src/site/markdown/docs/select.md @@ -10,7 +10,7 @@ In general, the following are supported: 2. Tables can be aliased per select statement 3. Columns can be aliased per select statement 4. Some support for aggregates (avg, min, max, sum) -5. Equijoins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER +5. Joins of type INNER, LEFT OUTER, RIGHT OUTER, FULL OUTER 6. Subqueries in where clauses. For example, `where foo in (select foo from foos where id < 36)` 7. Select from another select. For example `select count(*) from (select foo from foos where id < 36)` 8. Multi-Selects. For example `(select * from foo order by id limit 3) union (select * from foo order by id desc limit 3)` @@ -21,47 +21,50 @@ At this time, the library does not support the following: 2. INTERSECT, EXCEPT, etc. The user guide page for WHERE Clauses shows examples of many types of SELECT statements with different complexities of -the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY clause: +the WHERE clause including support for sub-queries. We will just show a single example here, including an ORDER BY +clause: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .where(id, isIn(1, 5, 7)) - .and(bodyWeight, isBetween(1.0).and(3.0)) - .orderBy(id.descending(), bodyWeight) - .build() - .render(RenderingStrategies.MYBATIS3); - - List animals = mapper.selectMany(selectStatement); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .where(id, isIn(1, 5, 7)) + .and(bodyWeight, isBetween(1.0).and(3.0)) + .orderBy(id.descending(), bodyWeight) + .build() + .render(RenderingStrategies.MYBATIS3); + +List animals = mapper.selectMany(selectStatement); ``` The WHERE and ORDER BY clauses are optional. ## Joins -The library supports the generation of equijoin statements - joins defined by column matching. For example: +The library supports the generation of join statements. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) - .from(orderMaster, "om") - .join(orderDetail, "od").on(orderMaster.orderId, equalTo(orderDetail.orderId)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderDetail.lineNumber, orderDetail.description, orderDetail.quantity) + .from(orderMaster, "om") + .join(orderDetail, "od").on(orderMaster.orderId, isEqualTo(orderDetail.orderId)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be used in the generated SQL. +Notice that you can give an alias to a table if desired. If you don't specify an alias, the full table name will be +used in the generated SQL. Multiple tables can be joined in a single statement. For example: ```java - SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) - .from(orderMaster, "om") - .join(orderLine, "ol").on(orderMaster.orderId, equalTo(orderLine.orderId)) - .join(itemMaster, "im").on(orderLine.itemId, equalTo(itemMaster.itemId)) - .where(orderMaster.orderId, isEqualTo(2)) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDate, orderLine.lineNumber, itemMaster.description, orderLine.quantity) + .from(orderMaster, "om") + .join(orderLine, "ol").on(orderMaster.orderId, isEqualTo(orderLine.orderId)) + .join(itemMaster, "im").on(orderLine.itemId, isEqualTo(itemMaster.itemId)) + .where(orderMaster.orderId, isEqualTo(2)) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is required. This is due to the limitations of the MyBatis annotations when mapping collections. +Join queries will likely require you to define a MyBatis result mapping in XML. This is the only instance where XML is +required. This is due to the limitations of the MyBatis annotations when mapping collections. The library supports four join types: @@ -74,14 +77,14 @@ The library supports four join types: The library supports the generation of UNION and UNION ALL queries. For example: ```java - SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .union() - .selectDistinct(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .union() + .selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .build() + .render(RenderingStrategies.MYBATIS3); ``` Any number of SELECT statements can be added to a UNION query. Only one ORDER BY phrase is allowed. @@ -96,16 +99,16 @@ Multi-select queries are a special case of union select statements. The differen paging clauses can be applied to the merged queries. For example: ```java - SelectStatementProvider selectStatement = multiSelect( - select(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id) - .limit(2) +SelectStatementProvider selectStatement = multiSelect( + select(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id) + .limit(2) ).union( - selectDistinct(id, animalName, bodyWeight, brainWeight) - .from(animalData) - .orderBy(id.descending()) - .limit(3) + selectDistinct(id, animalName, bodyWeight, brainWeight) + .from(animalData) + .orderBy(id.descending()) + .limit(3) ) .build() .render(RenderingStrategies.MYBATIS3); @@ -114,7 +117,8 @@ paging clauses can be applied to the merged queries. For example: ## MyBatis Mapper for Select Statements The SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. If you -are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" and a "selectOne" method with a shared result mapping): +are using an annotated mapper, the select method should look like this (note that we recommend coding a "selectMany" +and a "selectOne" method with a shared result mapping): ```java import org.apache.ibatis.annotations.Result; @@ -143,7 +147,9 @@ import org.mybatis.dynamic.sql.util.SqlProviderAdapter; ## XML Mapper for Join Statements -If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks like this: +If you are coding a join, it is likely you will need to code an XML mapper to define the result map. This is due to a +MyBatis limitation - the annotations cannot define a collection mapping. If you have to do this, the Java code looks +like this: ```java @SelectProvider(type=SqlProviderAdapter.class, method="select") @@ -171,7 +177,8 @@ And the corresponding XML looks like this: Notice that the resultMap is the only element in the XML mapper. This is our recommended practice. ## XML Mapper for Select Statements -We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider object can be used as a parameter to a MyBatis mapper method directly. +We do not recommend using an XML mapper for select statements, but if you want to do so the SelectStatementProvider +object can be used as a parameter to a MyBatis mapper method directly. If you are using an XML mapper, the select method should look like this in the Java interface: @@ -205,30 +212,33 @@ Order by phrases can be difficult to calculate when there are aliased columns, a This library has taken a relatively simple approach: 1. When specifying an SqlColumn in an ORDER BY phrase the library will either write the column alias or the column -name into the ORDER BY phrase. For the ORDER BY phrase, the table alias (if there is one) will be ignored. Use this pattern -when the ORDER BY column is a member of the select list. For example `orderBy(foo)`. If the column has an alias, then -it is easist to use the "arbitrary string" method with the column alias as shown below. -1. It is also possible to explicitly specify a table alias for a column in an ORDER BY phrase. Use this pattern when -there is a join, and the ORDER BY column is in two or more tables, and the ORDER BY column is not in the select -list. For example `orderBy(sortColumn("t1", foo))`. -1. If none of the above use cases meet your needs, then you can specify an arbitrary String to write into the rendered ORDER BY -phrase (see below for an example). + name into the ORDER BY phrase. For the ORDER BY phrase, the table alias (if there is one) will be ignored. Use this + pattern when the ORDER BY column is a member of the select list. For example `orderBy(foo)`. If the column has an + alias, then it is easiest to use the "arbitrary string" method with the column alias as shown below. +2. It is also possible to explicitly specify a table alias for a column in an ORDER BY phrase. Use this pattern when + there is a join, and the ORDER BY column is in two or more tables, and the ORDER BY column is not in the select + list. For example `orderBy(sortColumn("t1", foo))`. +3. If none of the above use cases meet your needs, then you can specify an arbitrary String to write into the rendered + ORDER BY phrase (see below for an example). In our testing, this caused an issue in only one case. When there is an outer join and the select list contains both the left and right join column. In that case, the workaround is to supply a column alias for both columns. -When using a column function (lower, upper, etc.), then it is customary to give the calculated column an alias so you will have a predictable result set. In cases like this there will not be a column to use for an alias. The library supports arbitrary values in an ORDER BY expression like this: +When using a column function (lower, upper, etc.), then it is customary to give the calculated column an alias so you +will have a predictable result set. In cases like this there will not be a column to use for an alias. The library +supports arbitrary values in an ORDER BY expression like this: ```java - SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) - .from(person, "a") - .groupBy(substring(gender, 1, 1)) - .orderBy(sortColumn("ShortGender").descending()) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(substring(gender, 1, 1).as("ShortGender"), avg(age).as("AverageAge")) + .from(person, "a") + .groupBy(substring(gender, 1, 1)) + .orderBy(sortColumn("ShortGender").descending()) + .build() + .render(RenderingStrategies.MYBATIS3); ``` -In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. +In this example the `substring` function is used in both the select list and the GROUP BY expression. In the ORDER BY +expression, we use the `sortColumn` function to duplicate the alias given to the column in the select list. ## Limit and Offset Support Since version 1.1.1 the select statement supports limit and offset for paging (or slicing) queries. You can specify: @@ -237,18 +247,22 @@ Since version 1.1.1 the select statement supports limit and offset for paging (o - Offset only - Both limit and offset -It is important to note that the select renderer writes limit and offset clauses into the generated select statement as is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. Therefore, it is very important for users to understand whether or not the target database supports limit and offset. If the target database does not support limit and offset, then it is likely that using this support will create SQL that has runtime errors. +It is important to note that the select renderer writes limit and offset clauses into the generated select statement as +is. The library does not attempt to normalize those values for databases that don't support limit and offset directly. +Therefore, it is very important for users to understand whether the target database supports limit and offset. +If the target database does not support limit and offset, then it is likely that using this support will create SQL +that has runtime errors. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .limit(3) - .offset(22) - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .limit(3) + .offset(22) + .build() + .render(RenderingStrategies.MYBATIS3); ``` ## Fetch First Support @@ -263,11 +277,11 @@ Fetch first is an SQL standard and is supported by most databases. An example follows: ```java - SelectStatementProvider selectStatement = select(animalData.allColumns()) - .from(animalData) - .orderBy(id) - .offset(22) - .fetchFirst(3).rowsOnly() - .build() - .render(RenderingStrategies.MYBATIS3); +SelectStatementProvider selectStatement = select(animalData.allColumns()) + .from(animalData) + .orderBy(id) + .offset(22) + .fetchFirst(3).rowsOnly() + .build() + .render(RenderingStrategies.MYBATIS3); ``` diff --git a/src/site/site.xml b/src/site/site.xml index e22bf264d..2a02c4a79 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -36,6 +36,7 @@

+ From f31473cba252f659bc0c1beabf4e6f0b95111160 Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 Aug 2024 17:13:38 -0400 Subject: [PATCH 8/9] Docs --- src/site/markdown/docs/migratingV1toV2.md | 50 +++++++---------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/src/site/markdown/docs/migratingV1toV2.md b/src/site/markdown/docs/migratingV1toV2.md index 8d51e68f9..f24fdaafc 100644 --- a/src/site/markdown/docs/migratingV1toV2.md +++ b/src/site/markdown/docs/migratingV1toV2.md @@ -1,45 +1,21 @@ # V1 to V2 Migration Guide -Version 2 of MyBatis Dynamic SQL introduced many new features. This page will document how to migrate code -from prior releases to version 2. +Version 2 of MyBatis Dynamic SQL introduced many new features. On this page we will provide examples for the more +significant changes - changes that are more substantial than following deprecation messages. -## Java Join Syntax - -Version2 offers a fully flexible join specification and reuses the capabilities of where clauses. Of course, -not all capabilities are supported in databases, but you should now be able to code any type of join specification. +## Kotlin Join Syntax +The Java DSL for joins was changed to allow much more flexible joins. Of course, not all capabilities are supported in +all databases, but you should now be able to code most joins specification that are supported by your database. The changes in the Java DSL are mostly internal and should not impact most users. The `equalTo` methods has been deprecated in favor of `isEqualTo`, but all other changes should be hidden. -V1 Join Specification Example: -```java -SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDetail.lineNumber, orderDetail.quantity) - .from(orderMaster, "om") - .join(orderDetail, "od", on(orderMaster.orderId, equalTo(orderDetail.orderId))) - .build() - .render(RenderingStrategies.MYBATIS3); -``` +Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and allows for much more flexible join +specifications. The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main +difference is that the "on" keyword should be moved outside the join specification lambda (it is now an infix function). +Inside the lambda, the conditions should be rewritten to match the syntax of a where clause. -V2 Join Specification Example: -```java -SelectStatementProvider selectStatement = select(orderMaster.orderId, orderDetail.lineNumber, orderDetail.quantity) - .from(orderMaster, "om") - .join(orderDetail, "od", on(orderMaster.orderId, isEqualTo(orderDetail.orderId))) - .build() - .render(RenderingStrategies.MYBATIS3); -``` - -## Kotlin Join Syntax - -Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and reuses the capabilities of where -clauses. Of course, not all capabilities are supported in databases, but you should now be able to code any type of -join specification. - -The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main difference is that -the "on" keyword should be moved outside the join specification lambda (it is now an infix function). Inside the lambda, -the conditions should be rewritten to match the syntax of a where clause. - -V1 Join Specification Example: +V1 (Deprecated) Join Specification Example: ```kotlin val selectStatement = select( orderMaster.orderId, orderMaster.orderDate, @@ -65,5 +41,9 @@ val selectStatement = select( and { orderMaster.orderId isEqualTo constant("1") } } } - ``` + +Notice that the "on" keyword has been moved outside the lambda, and the conditions are coded with the same syntax used +by WHERE, HAVING, and CASE expressions. + +The prior syntax is deprecated and will be removed in a future release. From 0f1ad9c33f2f64033eb61f00f2ff1da42536948e Mon Sep 17 00:00:00 2001 From: Jeff Butler Date: Mon, 19 Aug 2024 17:25:46 -0400 Subject: [PATCH 9/9] Checkstyle --- src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java | 4 ++-- .../java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java | 2 +- .../org/mybatis/dynamic/sql/select/QueryExpressionDSL.java | 3 ++- src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java | 2 +- src/site/markdown/docs/migratingV1toV2.md | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java index 4468b523a..2cf6ac277 100644 --- a/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java +++ b/src/main/java/org/mybatis/dynamic/sql/SqlBuilder.java @@ -439,8 +439,8 @@ static ColumnAndConditionCriterion on(BindableColumn joinColumn, Visit * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(BasicColumn)}. * * @param column the column - * @return an IsEqualToColumn condition * @param the column type + * @return an IsEqualToColumn condition * @deprecated since 2.0.0. Please replace with isEqualTo(column) */ @Deprecated(since = "2.0.0", forRemoval = true) @@ -452,8 +452,8 @@ static IsEqualToColumn equalTo(BindableColumn column) { * Starting in version 2.0.0, this function is a synonym for {@link SqlBuilder#isEqualTo(Object)}. * * @param value the value - * @return an IsEqualTo condition * @param the column type + * @return an IsEqualTo condition * @deprecated since 2.0.0. Please replace with isEqualTo(value) */ @Deprecated(since = "2.0.0", forRemoval = true) 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 42a20e551..0c244481c 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/MultiSelectDSL.java @@ -77,7 +77,7 @@ public OffsetFirstFinisher offsetWhenPresent(Long offset) { @Override public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> MultiSelectDSL.this; + return () -> this; } @NotNull 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 034e9b593..c4e2b5d9a 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/QueryExpressionDSL.java @@ -359,7 +359,8 @@ public JoinSpecificationFinisher on(BindableColumn joinColumn, VisitableC public class JoinSpecificationFinisher extends AbstractBooleanExpressionDSL - implements AbstractWhereStarter, Buildable, PagingDSL { + implements AbstractWhereStarter, Buildable, + PagingDSL { private final TableExpression table; private final JoinType joinType; 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 4b5ef2077..525e3282d 100644 --- a/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java +++ b/src/main/java/org/mybatis/dynamic/sql/select/SelectDSL.java @@ -119,7 +119,7 @@ public OffsetFirstFinisher offsetWhenPresent(Long offset) { public FetchFirstFinisher fetchFirstWhenPresent(Long fetchFirstRows) { this.fetchFirstRows = fetchFirstRows; - return () -> SelectDSL.this; + return () -> this; } @Override diff --git a/src/site/markdown/docs/migratingV1toV2.md b/src/site/markdown/docs/migratingV1toV2.md index f24fdaafc..12b6adc37 100644 --- a/src/site/markdown/docs/migratingV1toV2.md +++ b/src/site/markdown/docs/migratingV1toV2.md @@ -11,7 +11,7 @@ The changes in the Java DSL are mostly internal and should not impact most users deprecated in favor of `isEqualTo`, but all other changes should be hidden. Like the Java DSL, the V2 Kotlin DSL offers a fully flexible join specification and allows for much more flexible join -specifications. The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main +specifications. The changes in the Kotlin DSL allow a more natural expressions of a join specification. The main difference is that the "on" keyword should be moved outside the join specification lambda (it is now an infix function). Inside the lambda, the conditions should be rewritten to match the syntax of a where clause.