From 01e8201294bf877604d2f32434112d8361842409 Mon Sep 17 00:00:00 2001 From: Tom Van Wemmel Date: Tue, 12 Nov 2024 12:46:52 +0100 Subject: [PATCH] Add support for NOT_IN, NOT_LIKE and NEGATING_SIMPLE_PROPERTY Closes spring-projects/spring-data-keyvalue#603 --- .../query/PredicateQueryCreator.java | 7 ++ .../repository/query/SpelQueryCreator.java | 18 ++++- .../query/AbstractQueryCreatorTestBase.java | 78 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java index 43314dc6..99ae9ff1 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java @@ -41,6 +41,7 @@ * {@link AbstractQueryCreator} to create {@link Predicate}-based {@link KeyValueQuery}s. * * @author Christoph Strobl + * @author Tom Van Wemmel * @since 3.3 */ public class PredicateQueryCreator extends AbstractQueryCreator>, Predicate> { @@ -59,12 +60,16 @@ protected Predicate create(Part part, Iterator iterator) { return PredicateBuilder.propertyValueOf(part).isFalse(); case SIMPLE_PROPERTY: return PredicateBuilder.propertyValueOf(part).isEqualTo(iterator.next()); + case NEGATING_SIMPLE_PROPERTY: + return PredicateBuilder.propertyValueOf(part).isEqualTo(iterator.next()).negate(); case IS_NULL: return PredicateBuilder.propertyValueOf(part).isNull(); case IS_NOT_NULL: return PredicateBuilder.propertyValueOf(part).isNotNull(); case LIKE: return PredicateBuilder.propertyValueOf(part).contains(iterator.next()); + case NOT_LIKE: + return PredicateBuilder.propertyValueOf(part).contains(iterator.next()).negate(); case STARTING_WITH: return PredicateBuilder.propertyValueOf(part).startsWith(iterator.next()); case AFTER: @@ -86,6 +91,8 @@ protected Predicate create(Part part, Iterator iterator) { return PredicateBuilder.propertyValueOf(part).matches(iterator.next()); case IN: return PredicateBuilder.propertyValueOf(part).in(iterator.next()); + case NOT_IN: + return PredicateBuilder.propertyValueOf(part).in(iterator.next()).negate(); default: throw new InvalidDataAccessApiUsageException(String.format("Found invalid part '%s' in query", part.getType())); diff --git a/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java b/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java index d0107e5e..f481ba67 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/query/SpelQueryCreator.java @@ -37,6 +37,7 @@ * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch + * @author Tom Van Wemmel */ public class SpelQueryCreator extends AbstractQueryCreator, String> { @@ -120,6 +121,9 @@ protected SpelExpression toPredicateExpression(PartTree tree) { case SIMPLE_PROPERTY: partBuilder.append("?.equals(").append("[").append(parameterIndex++).append("])"); break; + case NEGATING_SIMPLE_PROPERTY: + partBuilder.append("?.equals(").append("[").append(parameterIndex++).append("]) == false"); + break; case IS_NULL: partBuilder.append(" == null"); break; @@ -129,6 +133,9 @@ protected SpelExpression toPredicateExpression(PartTree tree) { case LIKE: partBuilder.append("?.contains(").append("[").append(parameterIndex++).append("])"); break; + case NOT_LIKE: + partBuilder.append("?.contains(").append("[").append(parameterIndex++).append("]) == false"); + break; case STARTING_WITH: partBuilder.append("?.startsWith(").append("[").append(parameterIndex++).append("])"); break; @@ -175,9 +182,16 @@ protected SpelExpression toPredicateExpression(PartTree tree) { partBuilder.append(")"); break; + case NOT_IN: + + partBuilder.append("[").append(parameterIndex++).append("].contains("); + partBuilder.append("#it?."); + partBuilder.append(part.getProperty().toDotPath().replace(".", "?.")); + partBuilder.append(") == false"); + break; + case CONTAINING: case NOT_CONTAINING: - case NEGATING_SIMPLE_PROPERTY: case EXISTS: default: throw new InvalidDataAccessApiUsageException( @@ -206,6 +220,6 @@ protected SpelExpression toPredicateExpression(PartTree tree) { } private static boolean requiresInverseLookup(Part part) { - return part.getType() == Type.IN; + return part.getType() == Type.IN || part.getType() == Type.NOT_IN; } } diff --git a/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java b/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java index 1d11e468..04a18529 100644 --- a/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java +++ b/src/test/java/org/springframework/data/keyvalue/repository/query/AbstractQueryCreatorTestBase.java @@ -43,6 +43,7 @@ /** * @author Christoph Strobl + * @author Tom Van Wemmel */ @ExtendWith(MockitoExtension.class) public abstract class AbstractQueryCreatorTestBase, ?>, CRITERIA> { @@ -70,6 +71,17 @@ void equalsReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstname", BRAN.firstname).against(RICKON)).isFalse(); } + @Test + // GH-603 + void notEqualsReturnsTrueWhenMatching() { + assertThat(evaluate("findByFirstnameNot", BRAN.firstname).against(RICKON)).isTrue(); + } + + @Test // GH-603 + void notEqualsReturnsFalseWhenNotMatching() { + assertThat(evaluate("findByFirstnameNot", BRAN.firstname).against(BRAN)).isFalse(); + } + @Test // DATACMNS-525 void isTrueAssertedProperlyWhenTrue() { assertThat(evaluate("findBySkinChangerIsTrue").against(BRAN)).isTrue(); @@ -130,6 +142,16 @@ void likeReturnsFalseWhenNotMatching() { assertThat(evaluate("findByFirstnameLike", "ra").against(ROBB)).isFalse(); } + @Test // GH-603 + void notLikeReturnsTrueWhenMatching() { + assertThat(evaluate("findByFirstnameNotLike", "ra").against(ROBB)).isTrue(); + } + + @Test // GH-603 + void notLikeReturnsFalseWhenNotMatching() { + assertThat(evaluate("findByFirstnameNotLike", "ob").against(ROBB)).isFalse(); + } + @Test // DATACMNS-525 void endsWithReturnsTrueWhenMatching() { assertThat(evaluate("findByFirstnameEndingWith", "bb").against(ROBB)).isTrue(); @@ -310,6 +332,53 @@ void inMatchesNullValuesCorrectly() { .isTrue(); } + @Test // GH-603 + void notInReturnsMatchCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameNotIn", list).against(JON)).isTrue(); + } + + @Test // GH-603 + void notInNotMatchingReturnsCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameNotIn", list).against(ROBB)).isFalse(); + } + + @Test // GH-603 + void notInWithNullCompareValuesCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(null); + + assertThat(evaluate("findByFirstnameNotIn", list).against(JON)).isTrue(); + } + + @Test // GH-603 + void notInWithNullSourceValuesMatchesCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(ROBB.firstname); + + assertThat(evaluate("findByFirstnameNotIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) + .isTrue(); + } + + @Test // GH-603 + void notInMatchesNullValuesCorrectly() { + + ArrayList list = new ArrayList<>(); + list.add(null); + + assertThat(evaluate("findByFirstnameNotIn", list).against(new PredicateQueryCreatorUnitTests.Person(null, 10))) + .isFalse(); + } + @Test // DATAKV-185 void noDerivedQueryArgumentsMatchesAlways() { @@ -363,6 +432,9 @@ interface PersonRepository extends CrudRepository { // Type.SIMPLE_PROPERTY Person findByFirstname(String firstname); + // Type.NEGATING_SIMPLE_PROPERTY + Person findByFirstnameNot(String firstname); + // Type.TRUE Person findBySkinChangerIsTrue(); @@ -404,6 +476,9 @@ interface PersonRepository extends CrudRepository { // Type.LIKE Person findByFirstnameLike(String firstname); + // Type.NOT_LIKE + Person findByFirstnameNotLike(String firstname); + // Type.ENDING_WITH Person findByFirstnameEndingWith(String firstname); @@ -417,6 +492,9 @@ interface PersonRepository extends CrudRepository { // Type.IN Person findByFirstnameIn(ArrayList in); + // Type.NOT_IN + Person findByFirstnameNotIn(ArrayList in); + } public interface Evaluation {