Skip to content

Commit 3727c6d

Browse files
committed
feat: add andAny() and orAny() methods for combining predicates with different types (#342)
Added new methods to SearchFieldPredicate interface to allow combining predicates with different field types, solving the generic type incompatibility issue when building complex queries. The existing and() and or() methods require compatible types due to Java's type system constraints. The new andAny() and orAny() methods use wildcards to allow combining predicates regardless of their field types, enabling queries like: - NAME_SPACE.eq("PERSONAL").andAny(RELATE_ID.eq(100L))
1 parent ebaf49f commit 3727c6d

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/SearchFieldPredicate.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,48 @@ default Predicate<T> and(Predicate<? super T> other) {
7474
return andPredicate;
7575
}
7676

77+
/**
78+
* Combines this predicate with another predicate of a potentially different field type
79+
* using a logical OR operation.
80+
*
81+
* <p>This method allows combining predicates with different field types, which is useful
82+
* when building complex queries across multiple fields of different types.</p>
83+
*
84+
* @param <U> the field type of the other predicate
85+
* @param other the predicate to combine with this one
86+
* @return a new OR predicate combining both predicates
87+
*/
88+
@SuppressWarnings(
89+
{ "unchecked", "rawtypes" }
90+
)
91+
default <U> SearchFieldPredicate<E, ?> orAny(SearchFieldPredicate<E, U> other) {
92+
Objects.requireNonNull(other);
93+
OrPredicate orPredicate = new OrPredicate(this);
94+
orPredicate.addPredicate(other);
95+
return orPredicate;
96+
}
97+
98+
/**
99+
* Combines this predicate with another predicate of a potentially different field type
100+
* using a logical AND operation.
101+
*
102+
* <p>This method allows combining predicates with different field types, which is useful
103+
* when building complex queries across multiple fields of different types.</p>
104+
*
105+
* @param <U> the field type of the other predicate
106+
* @param other the predicate to combine with this one
107+
* @return a new AND predicate combining both predicates
108+
*/
109+
@SuppressWarnings(
110+
{ "unchecked", "rawtypes" }
111+
)
112+
default <U> SearchFieldPredicate<E, ?> andAny(SearchFieldPredicate<E, U> other) {
113+
Objects.requireNonNull(other);
114+
AndPredicate andPredicate = new AndPredicate(this);
115+
andPredicate.addPredicate(other);
116+
return andPredicate;
117+
}
118+
77119
/**
78120
* Applies this predicate to a RediSearch query node.
79121
* This method transforms the predicate into a RediSearch query node
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.redis.om.spring.search.stream.predicates;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.lang.reflect.Field;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import com.redis.om.spring.metamodel.SearchFieldAccessor;
10+
import com.redis.om.spring.metamodel.indexed.NumericField;
11+
import com.redis.om.spring.metamodel.indexed.TagField;
12+
13+
/**
14+
* Unit test for issue #342: Verifying that predicates with different field types
15+
* can be combined using the new andAny() and orAny() methods.
16+
*/
17+
class MixedTypePredicatesTest {
18+
19+
static class TestEntity {
20+
private String nameSpace;
21+
private Long relateId;
22+
private String status;
23+
24+
public String getNameSpace() { return nameSpace; }
25+
public void setNameSpace(String nameSpace) { this.nameSpace = nameSpace; }
26+
27+
public Long getRelateId() { return relateId; }
28+
public void setRelateId(Long relateId) { this.relateId = relateId; }
29+
30+
public String getStatus() { return status; }
31+
public void setStatus(String status) { this.status = status; }
32+
}
33+
34+
@Test
35+
void testIssue342_CanCombineDifferentTypePredicatesWithAndAny() throws NoSuchFieldException {
36+
// Create field accessors
37+
Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace");
38+
Field relateIdField = TestEntity.class.getDeclaredField("relateId");
39+
40+
SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField);
41+
SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField);
42+
43+
// Create predicates with different types
44+
TagField<TestEntity, String> nameSpacePredicate = new TagField<>(nameSpaceAccessor, true);
45+
NumericField<TestEntity, Long> relateIdPredicate = new NumericField<>(relateIdAccessor, true);
46+
47+
// Test that we can combine String and Long predicates with andAny()
48+
SearchFieldPredicate<TestEntity, String> stringPred = nameSpacePredicate.eq("PERSONAL");
49+
SearchFieldPredicate<TestEntity, Long> longPred = relateIdPredicate.eq(100L);
50+
51+
// This should compile without type errors
52+
SearchFieldPredicate<TestEntity, ?> combined = stringPred.andAny(longPred);
53+
assertNotNull(combined);
54+
assertTrue(combined instanceof AndPredicate);
55+
56+
// Verify the predicate was added
57+
AndPredicate<TestEntity, ?> andPred = (AndPredicate<TestEntity, ?>) combined;
58+
assertEquals(2, andPred.stream().count());
59+
}
60+
61+
@Test
62+
void testIssue342_CanCombineDifferentTypePredicatesWithOrAny() throws NoSuchFieldException {
63+
// Create field accessors
64+
Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace");
65+
Field relateIdField = TestEntity.class.getDeclaredField("relateId");
66+
67+
SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField);
68+
SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField);
69+
70+
// Create predicates with different types
71+
TagField<TestEntity, String> nameSpacePredicate = new TagField<>(nameSpaceAccessor, true);
72+
NumericField<TestEntity, Long> relateIdPredicate = new NumericField<>(relateIdAccessor, true);
73+
74+
// Test that we can combine String and Long predicates with orAny()
75+
SearchFieldPredicate<TestEntity, String> stringPred = nameSpacePredicate.eq("BUSINESS");
76+
SearchFieldPredicate<TestEntity, Long> longPred = relateIdPredicate.eq(200L);
77+
78+
// This should compile without type errors
79+
SearchFieldPredicate<TestEntity, ?> combined = stringPred.orAny(longPred);
80+
assertNotNull(combined);
81+
assertTrue(combined instanceof OrPredicate);
82+
83+
// Verify the predicate was added
84+
OrPredicate<TestEntity, ?> orPred = (OrPredicate<TestEntity, ?>) combined;
85+
assertEquals(2, orPred.stream().count());
86+
}
87+
88+
@Test
89+
void testIssue342_ChainMultipleDifferentTypes() throws NoSuchFieldException {
90+
// Create field accessors
91+
Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace");
92+
Field relateIdField = TestEntity.class.getDeclaredField("relateId");
93+
Field statusField = TestEntity.class.getDeclaredField("status");
94+
95+
SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField);
96+
SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField);
97+
SearchFieldAccessor statusAccessor = new SearchFieldAccessor("@status", "$.status", statusField);
98+
99+
// Create predicates with different types
100+
TagField<TestEntity, String> nameSpacePredicate = new TagField<>(nameSpaceAccessor, true);
101+
NumericField<TestEntity, Long> relateIdPredicate = new NumericField<>(relateIdAccessor, true);
102+
TagField<TestEntity, String> statusPredicate = new TagField<>(statusAccessor, true);
103+
104+
// Test chaining multiple different types
105+
SearchFieldPredicate<TestEntity, ?> combined = nameSpacePredicate.eq("PERSONAL")
106+
.andAny(relateIdPredicate.eq(100L))
107+
.andAny(statusPredicate.eq("ACTIVE"));
108+
109+
assertNotNull(combined);
110+
assertTrue(combined instanceof AndPredicate);
111+
}
112+
113+
@Test
114+
void testIssue342_MixAndAnyAndOrAny() throws NoSuchFieldException {
115+
// Create field accessors
116+
Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace");
117+
Field relateIdField = TestEntity.class.getDeclaredField("relateId");
118+
119+
SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField);
120+
SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField);
121+
122+
// Create predicates with different types
123+
TagField<TestEntity, String> nameSpacePredicate = new TagField<>(nameSpaceAccessor, true);
124+
NumericField<TestEntity, Long> relateIdPredicate = new NumericField<>(relateIdAccessor, true);
125+
126+
// Test mixing andAny and orAny
127+
SearchFieldPredicate<TestEntity, ?> combined = nameSpacePredicate.eq("PERSONAL")
128+
.andAny(relateIdPredicate.gt(50L))
129+
.orAny(nameSpacePredicate.eq("BUSINESS"));
130+
131+
assertNotNull(combined);
132+
assertTrue(combined instanceof OrPredicate);
133+
}
134+
135+
@Test
136+
void testIssue342_SameTypeStillWorksWithRegularAnd() throws NoSuchFieldException {
137+
// Create field accessors for same type
138+
Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace");
139+
Field statusField = TestEntity.class.getDeclaredField("status");
140+
141+
SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField);
142+
SearchFieldAccessor statusAccessor = new SearchFieldAccessor("@status", "$.status", statusField);
143+
144+
// Create predicates with same type (String)
145+
TagField<TestEntity, String> nameSpacePredicate = new TagField<>(nameSpaceAccessor, true);
146+
TagField<TestEntity, String> statusPredicate = new TagField<>(statusAccessor, true);
147+
148+
// Test that same-type predicates still work with regular and()
149+
// Note: and() returns Predicate<T>, not SearchFieldPredicate, so we use andAny for consistency
150+
SearchFieldPredicate<TestEntity, ?> combined = nameSpacePredicate.eq("PERSONAL")
151+
.andAny(statusPredicate.eq("ACTIVE"));
152+
153+
assertNotNull(combined);
154+
assertTrue(combined instanceof AndPredicate);
155+
156+
// Verify both predicates are included
157+
@SuppressWarnings("unchecked")
158+
AndPredicate<TestEntity, ?> andPred = (AndPredicate<TestEntity, ?>) combined;
159+
assertEquals(2, andPred.stream().count());
160+
}
161+
}

0 commit comments

Comments
 (0)