Skip to content

Commit 842a309

Browse files
committed
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.
1 parent 38139d7 commit 842a309

File tree

8 files changed

+243
-5
lines changed

8 files changed

+243
-5
lines changed

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import static java.util.Arrays.*;
1919
import static org.assertj.core.api.Assertions.*;
2020
import static org.assertj.core.api.SoftAssertions.*;
21-
import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*;
2221
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
2322

2423
import lombok.Data;
@@ -44,7 +43,6 @@
4443
import org.springframework.data.jdbc.repository.query.Query;
4544
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
4645
import org.springframework.data.jdbc.testing.AssumeFeatureRule;
47-
import org.springframework.data.jdbc.testing.EnabledOnFeature;
4846
import org.springframework.data.jdbc.testing.TestConfiguration;
4947
import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
5048
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
@@ -339,6 +337,38 @@ public void existsWorksAsExpected() {
339337
});
340338
}
341339

340+
@Test // DATAJDBC-604
341+
public void existsInWorksAsExpected() {
342+
343+
DummyEntity dummy = repository.save(createDummyEntity());
344+
345+
assertSoftly(softly -> {
346+
347+
softly.assertThat(repository.existsByNameIn(dummy.getName())) //
348+
.describedAs("Positive") //
349+
.isTrue();
350+
softly.assertThat(repository.existsByNameIn()) //
351+
.describedAs("Negative") //
352+
.isFalse();
353+
});
354+
}
355+
356+
@Test // DATAJDBC-604
357+
public void existsNotInWorksAsExpected() {
358+
359+
DummyEntity dummy = repository.save(createDummyEntity());
360+
361+
assertSoftly(softly -> {
362+
363+
softly.assertThat(repository.existsByNameNotIn(dummy.getName())) //
364+
.describedAs("Positive") //
365+
.isFalse();
366+
softly.assertThat(repository.existsByNameNotIn()) //
367+
.describedAs("Negative") //
368+
.isTrue();
369+
});
370+
}
371+
342372
@Test // DATAJDBC-534
343373
public void countByQueryDerivation() {
344374

@@ -370,6 +400,10 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
370400
@Query("SELECT id_Prop from dummy_entity where id_Prop = :id")
371401
DummyEntity withMissingColumn(@Param("id") Long id);
372402

403+
boolean existsByNameIn(String... names);
404+
405+
boolean existsByNameNotIn(String... names);
406+
373407
boolean existsByName(String name);
374408

375409
int countByName(String name);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.sql;
17+
18+
/**
19+
* Simple condition that evaluates to SQL {@code FALSE}.
20+
*
21+
* @author Mark Paluch
22+
* @since 2.1
23+
*/
24+
public class FalseCondition implements Condition {
25+
26+
public static final FalseCondition INSTANCE = new FalseCondition();
27+
28+
private FalseCondition() {}
29+
30+
/*
31+
* (non-Javadoc)
32+
* @see java.lang.Object#toString()
33+
*/
34+
@Override
35+
public String toString() {
36+
return "1 = 0";
37+
}
38+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/In.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public static In createNotIn(Expression columnOrExpression, Expression... expres
149149
return new In(columnOrExpression, Arrays.asList(expressions), true);
150150
}
151151

152-
/*
152+
/*
153153
* (non-Javadoc)
154154
* @see org.springframework.data.relational.core.sql.Condition#not()
155155
*/
@@ -158,13 +158,26 @@ public Condition not() {
158158
return new In(left, expressions, !notIn);
159159
}
160160

161+
/**
162+
* @return {@code true} if this condition has at least one expression.
163+
* @since 2.1
164+
*/
165+
public boolean hasExpressions() {
166+
return !expressions.isEmpty();
167+
}
168+
161169
/*
162170
* (non-Javadoc)
163171
* @see java.lang.Object#toString()
164172
*/
165173
@Override
166174
public String toString() {
167-
return left + (notIn ? " NOT" : "") + " IN (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")";
175+
176+
if (hasExpressions()) {
177+
return left + (notIn ? " NOT" : "") + " IN (" + StringUtils.collectionToDelimitedString(expressions, ", ") + ")";
178+
}
179+
180+
return notIn ? TrueCondition.INSTANCE.toString() : FalseCondition.INSTANCE.toString();
168181
}
169182

170183
public boolean isNotIn() {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.sql;
17+
18+
/**
19+
* Simple condition that evaluates to SQL {@code TRUE}.
20+
*
21+
* @author Mark Paluch
22+
* @since 2.1
23+
*/
24+
public class TrueCondition implements Condition {
25+
26+
public static final TrueCondition INSTANCE = new TrueCondition();
27+
28+
private TrueCondition() {}
29+
30+
/*
31+
* (non-Javadoc)
32+
* @see java.lang.Object#toString()
33+
*/
34+
@Override
35+
public String toString() {
36+
return "1 = 1";
37+
}
38+
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ConditionVisitor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,12 @@ private DelegatingVisitor getDelegation(Condition segment) {
8989
}
9090

9191
if (segment instanceof In) {
92-
return new InVisitor(context, builder::append);
92+
93+
if (((In) segment).hasExpressions()) {
94+
return new InVisitor(context, builder::append);
95+
} else {
96+
return new EmptyInVisitor(context, builder::append);
97+
}
9398
}
9499

95100
if (segment instanceof NestedCondition) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2019-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.sql.render;
17+
18+
import org.springframework.data.relational.core.sql.FalseCondition;
19+
import org.springframework.data.relational.core.sql.In;
20+
import org.springframework.data.relational.core.sql.TrueCondition;
21+
22+
/**
23+
* Renderer for empty {@link In}. Uses a {@link RenderTarget} to call back for render results.
24+
*
25+
* @author Mark Paluch
26+
* @since 2.1
27+
*/
28+
class EmptyInVisitor extends TypedSingleConditionRenderSupport<In> {
29+
30+
private final RenderTarget target;
31+
32+
EmptyInVisitor(RenderContext context, RenderTarget target) {
33+
super(context);
34+
this.target = target;
35+
}
36+
37+
/*
38+
* (non-Javadoc)
39+
* @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable)
40+
*/
41+
@Override
42+
Delegation leaveMatched(In segment) {
43+
44+
target.onRendered(segment.isNotIn() ? TrueCondition.INSTANCE.toString() : FalseCondition.INSTANCE.toString());
45+
46+
return super.leaveMatched(segment);
47+
}
48+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.sql;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
/**
23+
* Unit tests for {@link In}.
24+
*
25+
* @author Mark Paluch
26+
*/
27+
public class InTests {
28+
29+
@Test // DATAJDBC-604
30+
void shouldRenderToString() {
31+
32+
Table table = Table.create("table");
33+
34+
assertThat(In.create(table.column("col"), SQL.bindMarker())).hasToString("table.col IN (?)");
35+
assertThat(In.create(table.column("col"), SQL.bindMarker()).not()).hasToString("table.col NOT IN (?)");
36+
}
37+
38+
@Test // DATAJDBC-604
39+
void shouldRenderEmptyExpressionToString() {
40+
41+
Table table = Table.create("table");
42+
43+
assertThat(In.create(table.column("col"))).hasToString("1 = 0");
44+
assertThat(In.create(table.column("col")).not()).hasToString("1 = 1");
45+
}
46+
}

spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/ConditionRendererUnitTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ public void shouldRenderIn() {
167167
assertThat(sql).endsWith("WHERE my_table.left IN (my_table.right)");
168168
}
169169

170+
@Test // DATAJDBC-604
171+
public void shouldRenderEmptyIn() {
172+
173+
String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table).where(left.in()).build());
174+
175+
assertThat(sql).endsWith("WHERE 1 = 0");
176+
}
177+
178+
@Test // DATAJDBC-604
179+
public void shouldRenderEmptyNotIn() {
180+
181+
String sql = SqlRenderer.toString(StatementBuilder.select(left).from(table).where(left.notIn()).build());
182+
183+
assertThat(sql).endsWith("WHERE 1 = 1");
184+
}
185+
170186
@Test // DATAJDBC-309
171187
public void shouldRenderLike() {
172188

0 commit comments

Comments
 (0)