From 842a309f27fd80d2c1fd76399482f84a5130f0d1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Sep 2020 09:58:10 +0200 Subject: [PATCH] DATAJDBC-604 - Support empty IN lists. We now support empty IN lists by rendering a condition that evaluates to FALSE using 1 = 0. For NOT IN, we render a condition that evaluates to TRUE using 1 = 1. --- .../JdbcRepositoryIntegrationTests.java | 38 ++++++++++++++- .../relational/core/sql/FalseCondition.java | 38 +++++++++++++++ .../data/relational/core/sql/In.java | 17 ++++++- .../relational/core/sql/TrueCondition.java | 38 +++++++++++++++ .../core/sql/render/ConditionVisitor.java | 7 ++- .../core/sql/render/EmptyInVisitor.java | 48 +++++++++++++++++++ .../data/relational/core/sql/InTests.java | 46 ++++++++++++++++++ .../render/ConditionRendererUnitTests.java | 16 +++++++ 8 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/FalseCondition.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TrueCondition.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/EmptyInVisitor.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/InTests.java diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 5e33271c..28d04403 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -18,7 +18,6 @@ package org.springframework.data.jdbc.repository; import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.*; -import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; import lombok.Data; @@ -44,7 +43,6 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.testing.AssumeFeatureRule; -import org.springframework.data.jdbc.testing.EnabledOnFeature; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; @@ -339,6 +337,38 @@ public class JdbcRepositoryIntegrationTests { }); } + @Test // DATAJDBC-604 + public void existsInWorksAsExpected() { + + DummyEntity dummy = repository.save(createDummyEntity()); + + assertSoftly(softly -> { + + softly.assertThat(repository.existsByNameIn(dummy.getName())) // + .describedAs("Positive") // + .isTrue(); + softly.assertThat(repository.existsByNameIn()) // + .describedAs("Negative") // + .isFalse(); + }); + } + + @Test // DATAJDBC-604 + public void existsNotInWorksAsExpected() { + + DummyEntity dummy = repository.save(createDummyEntity()); + + assertSoftly(softly -> { + + softly.assertThat(repository.existsByNameNotIn(dummy.getName())) // + .describedAs("Positive") // + .isFalse(); + softly.assertThat(repository.existsByNameNotIn()) // + .describedAs("Negative") // + .isTrue(); + }); + } + @Test // DATAJDBC-534 public void countByQueryDerivation() { @@ -370,6 +400,10 @@ public class JdbcRepositoryIntegrationTests { @Query("SELECT id_Prop from dummy_entity where id_Prop = :id") DummyEntity withMissingColumn(@Param("id") Long id); + boolean existsByNameIn(String... names); + + boolean existsByNameNotIn(String... names); + boolean existsByName(String name); int countByName(String name); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/FalseCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/FalseCondition.java new file mode 100644 index 00000000..4694f402 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/FalseCondition.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 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.springframework.data.relational.core.sql; + +/** + * Simple condition that evaluates to SQL {@code FALSE}. + * + * @author Mark Paluch + * @since 2.1 + */ +public class FalseCondition implements Condition { + + public static final FalseCondition INSTANCE = new FalseCondition(); + + private FalseCondition() {} + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "1 = 0"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java index 4c41c666..c9a11f3a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java @@ -149,7 +149,7 @@ public class In extends AbstractSegment implements Condition { return new In(columnOrExpression, Arrays.asList(expressions), true); } - /* + /* * (non-Javadoc) * @see org.springframework.data.relational.core.sql.Condition#not() */ @@ -158,13 +158,26 @@ public class In extends AbstractSegment implements Condition { return new In(left, expressions, !notIn); } + /** + * @return {@code true} if this condition has at least one expression. + * @since 2.1 + */ + public boolean hasExpressions() { + return !expressions.isEmpty(); + } + /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { - return left + (notIn ? " NOT" : "") + " IN (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")"; + + if (hasExpressions()) { + return left + (notIn ? " NOT" : "") + " IN (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")"; + } + + return notIn ? TrueCondition.INSTANCE.toString() : FalseCondition.INSTANCE.toString(); } public boolean isNotIn() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TrueCondition.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TrueCondition.java new file mode 100644 index 00000000..d5831b9e --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TrueCondition.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020 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.springframework.data.relational.core.sql; + +/** + * Simple condition that evaluates to SQL {@code TRUE}. + * + * @author Mark Paluch + * @since 2.1 + */ +public class TrueCondition implements Condition { + + public static final TrueCondition INSTANCE = new TrueCondition(); + + private TrueCondition() {} + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "1 = 1"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java index 49b1d33b..b0477ed2 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java @@ -89,7 +89,12 @@ class ConditionVisitor extends TypedSubtreeVisitor implements PartRen } if (segment instanceof In) { - return new InVisitor(context, builder::append); + + if (((In) segment).hasExpressions()) { + return new InVisitor(context, builder::append); + } else { + return new EmptyInVisitor(context, builder::append); + } } if (segment instanceof NestedCondition) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/EmptyInVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/EmptyInVisitor.java new file mode 100644 index 00000000..933cb691 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/EmptyInVisitor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2020 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.springframework.data.relational.core.sql.render; + +import org.springframework.data.relational.core.sql.FalseCondition; +import org.springframework.data.relational.core.sql.In; +import org.springframework.data.relational.core.sql.TrueCondition; + +/** + * Renderer for empty {@link In}. Uses a {@link RenderTarget} to call back for render results. + * + * @author Mark Paluch + * @since 2.1 + */ +class EmptyInVisitor extends TypedSingleConditionRenderSupport { + + private final RenderTarget target; + + EmptyInVisitor(RenderContext context, RenderTarget target) { + super(context); + this.target = target; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) + */ + @Override + Delegation leaveMatched(In segment) { + + target.onRendered(segment.isNotIn() ? TrueCondition.INSTANCE.toString() : FalseCondition.INSTANCE.toString()); + + return super.leaveMatched(segment); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/InTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/InTests.java new file mode 100644 index 00000000..470640d5 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/InTests.java @@ -0,0 +1,46 @@ +/* +* Copyright 2020 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.springframework.data.relational.core.sql; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link In}. + * + * @author Mark Paluch + */ +public class InTests { + + @Test // DATAJDBC-604 + void shouldRenderToString() { + + Table table = Table.create("table"); + + assertThat(In.create(table.column("col"), SQL.bindMarker())).hasToString("table.col IN (?)"); + assertThat(In.create(table.column("col"), SQL.bindMarker()).not()).hasToString("table.col NOT IN (?)"); + } + + @Test // DATAJDBC-604 + void shouldRenderEmptyExpressionToString() { + + Table table = Table.create("table"); + + assertThat(In.create(table.column("col"))).hasToString("1 = 0"); + assertThat(In.create(table.column("col")).not()).hasToString("1 = 1"); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java index 28b7c0bd..b9be32cd 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java @@ -167,6 +167,22 @@ public class ConditionRendererUnitTests { assertThat(sql).endsWith("WHERE my_table.left IN (my_table.right)"); } + @Test // DATAJDBC-604 + public void shouldRenderEmptyIn() { + + String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table).where(left.in()).build()); + + assertThat(sql).endsWith("WHERE 1 = 0"); + } + + @Test // DATAJDBC-604 + public void shouldRenderEmptyNotIn() { + + String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table).where(left.notIn()).build()); + + assertThat(sql).endsWith("WHERE 1 = 1"); + } + @Test // DATAJDBC-309 public void shouldRenderLike() {