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