From 382f0941d5e1a06646c487971196ae53e4bf5a98 Mon Sep 17 00:00:00 2001 From: kicktocode Date: Fri, 18 Apr 2025 21:25:03 +0530 Subject: [PATCH 1/3] WIP: temporary commit before rebase --- .../com/redis/search/query/filter/Condition.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java index 88f151e..be7ce0d 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Condition.java @@ -1,23 +1,29 @@ package com.redis.search.query.filter; +import java.util.List; + public interface Condition { String getQuery(); default Condition and(Condition condition) { - return new And(this, condition); + return new And(this, condition); } default Condition or(Condition condition) { - return new Or(this, condition); + return new Or(this, condition); + } + + static Condition orList(final Condition... conditions) { + return new OrList(conditions); } default Condition not() { - return new Not(this); + return new Not(this); } default Condition optional() { - return new Optional(this); + return new Optional(this); } } From b3873e4a922a9eefdf0765e9e66ca49e180a9b93 Mon Sep 17 00:00:00 2001 From: kicktocode Date: Mon, 21 Apr 2025 20:25:52 +0530 Subject: [PATCH 2/3] Implemented OR and ORLIST class along with valid test cases. --- build.gradle | 1 + .../query/filter/CompositeCondition.java | 6 +- .../com/redis/search/query/filter/Or.java | 61 ++++++++++++++- .../com/redis/search/query/filter/OrList.java | 75 ++++++++++++++++++ .../com/redis/query/QueryBuilderTests.java | 2 +- .../src/test/java/com/redis/query/TestOr.java | 67 ++++++++++++++++ .../test/java/com/redis/query/TestOrList.java | 77 +++++++++++++++++++ 7 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java create mode 100644 core/lettucemod-query/src/test/java/com/redis/query/TestOr.java create mode 100644 core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java diff --git a/build.gradle b/build.gradle index f332823..142a7b9 100644 --- a/build.gradle +++ b/build.gradle @@ -108,6 +108,7 @@ subprojects { testImplementation 'org.testcontainers:junit-jupiter' testImplementation group: 'com.redis', name: 'testcontainers-redis', version: testcontainersRedisVersion testImplementation group: 'com.redis', name: 'testcontainers-redis-enterprise', version: testcontainersRedisVersion + testImplementation 'org.mockito:mockito-core:5.11.0' } test { diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java index 650d76a..a2b2b15 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/CompositeCondition.java @@ -2,9 +2,9 @@ public class CompositeCondition implements Condition { - private final Condition left; - private final Condition right; - private final CharSequence delimiter; + protected final Condition left; + protected final Condition right; + protected final CharSequence delimiter; public CompositeCondition(CharSequence delimiter, Condition left, Condition right) { this.delimiter = delimiter; diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java index b2c61b3..89ee0f1 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java @@ -1,11 +1,70 @@ package com.redis.search.query.filter; +import java.util.StringJoiner; + +/** + * Represents a logical OR condition between two {@link Condition} objects. + *

+ * This class generates a query that joins the left and right conditions using + * the OR operator ('|') in Redis Search query syntax. + *

+ * Example output: + *

{@code
+ * (@field1:{value1})|(@field2:{value2})
+ * }
+ */ public class Or extends CompositeCondition { + /** + * The delimiter used for joining two conditions with OR. + */ public static final String DELIMITER = "|"; + /** + * Constructs an {@code Or} condition from two {@link Condition} objects. + * + * @param left The left condition + * @param right The right condition + */ public Or(Condition left, Condition right) { - super(DELIMITER, left, right); + super(DELIMITER, left, right); } + /** + * Builds a logical OR query between two sub-conditions. + *

+ * Logic: + * 1. If both left and right queries are present, returns them in the format "(@leftQuery)|(@rightQuery)". + * 2. If only one of them is present, returns that query directly wrapped in parentheses. + * 3. If both are missing or empty, returns an empty string. + * + * @return A string representing the logical query. + */ + @Override + public String getQuery() { + String leftQuery = (left != null) ? left.getQuery() : null; + String rightQuery = (right != null) ? right.getQuery() : null; + + // Treat empty or whitespace strings as null + if (rightQuery != null && rightQuery.trim().isEmpty()) { + rightQuery = null; + } + + boolean hasLeft = leftQuery != null && !leftQuery.isEmpty(); + boolean hasRight = rightQuery != null && !rightQuery.isEmpty(); + + if (hasLeft && hasRight) { + // Both sides present: (@leftQuery)|(@rightQuery) + return String.format("(%s)|(%s)", leftQuery, rightQuery); + } else if (hasLeft) { + // Only left side present: (@leftQuery) + return String.format("(%s)", leftQuery); + } else if (hasRight) { + // Only right side present: (@rightQuery) + return String.format("(%s)", rightQuery); + } else { + // Both sides missing or blank + return ""; + } + } } diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java new file mode 100644 index 0000000..432686c --- /dev/null +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/OrList.java @@ -0,0 +1,75 @@ +package com.redis.search.query.filter; + +import java.util.Arrays; +import java.util.List; +import java.util.StringJoiner; + +/** + * Represents a logical OR condition composed of multiple {@link Condition} elements. + *

+ * This class generates a query string where each condition is wrapped in parentheses + * and separated by the OR operator ('|'), suitable for use in Redis Search queries. + *

+ * Example output: + *

{@code
+ *     ( (query1)|(query2)|(query3) )
+ * }
+ */ +public class OrList implements Condition { + + /** + * The delimiter used to join conditions in an OR clause. + */ + public static final String OR_LIST_CONDITION_FORMAT = "|"; + + private final List conditions; + + /** + * Constructs an {@code OrList} with the given conditions. + * + * @param conditions One or more {@link Condition} objects to be ORed together. + */ + public OrList(Condition... conditions) { + this.conditions = Arrays.asList(conditions); + } + + /** + * Builds and returns the query string representing a logical OR condition. + *

+ * Logic: + *

    + *
  • Each valid (non-null and non-empty) condition query is wrapped in parentheses.
  • + *
  • All valid conditions are joined using the OR operator {@code |}.
  • + *
  • If fewer than two valid conditions are present, the query is returned without wrapping the entire string in extra parentheses.
  • + *
  • If no valid conditions are present, an empty string is returned.
  • + *
+ * + * @return A formatted query string representing the logical OR of valid conditions, + * or an empty string if none exist. + */ + @Override + public String getQuery() { + if (conditions == null || conditions.isEmpty()) { + return ""; + } + + StringJoiner joiner = new StringJoiner(OR_LIST_CONDITION_FORMAT); + int validConditionCounter = 0; + for (Condition condition : conditions) { + if (condition == null) { + continue; + } + + String query = condition.getQuery(); + if (query != null && !query.isEmpty()) { + joiner.add("(" + query + ")"); + validConditionCounter++; + } + } + if (joiner.length() == 0 || validConditionCounter < 2) { + return joiner.toString(); + } + + return "(" + joiner.toString() + ")"; + } +} diff --git a/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java b/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java index 5dc68cb..fc0924b 100644 --- a/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java +++ b/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java @@ -109,7 +109,7 @@ void testIntersectionBasic() { void testIntersectionNested() { Condition condition = text("name").term("mark").or("dvir").and(numeric("time").between(100, 200)) .and(numeric("created").lt(1000).not()); - assertEquals("@name:(mark|dvir) @time:[100 200] -@created:[-inf (1000]", condition.getQuery()); + assertEquals("@name:((mark)|(dvir)) @time:[100 200] -@created:[-inf (1000]", condition.getQuery()); } @Test diff --git a/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java b/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java new file mode 100644 index 0000000..8202d3a --- /dev/null +++ b/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java @@ -0,0 +1,67 @@ +package com.redis.query; + +import com.redis.search.query.filter.Condition; +import com.redis.search.query.filter.Or; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestOr { + Condition left; + Condition right; + + @BeforeEach + public void setup() { + left = Mockito.mock(Condition.class); + right = Mockito.mock(Condition.class); + } + + @Test + public void testOrCondition_whenLeftRightIsPresent_thenReturnQuery() { + + Mockito.when(left.getQuery()).thenReturn("@category:{italian}"); + Mockito.when(right.getQuery()).thenReturn("@priceForTwo:[1500 2000]"); + + Or orCondition = new Or(left, right); + String orQuery = orCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})|(@priceForTwo:[1500 2000])", orQuery); + } + + @Test + public void testOrCondition_whenRightIsNullLeftIsPresent_thenReturnQuery() { + + Mockito.when(left.getQuery()).thenReturn("@category:{italian}"); + Mockito.when(right.getQuery()).thenReturn(" "); + + Or orCondition = new Or(left, right); + String orQuery = orCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})", orQuery); + } + + @Test + public void testOrCondition_whenRightIsEmptyLeftIsEmpty_thenReturnEmptyQuery() { + + Mockito.when(left.getQuery()).thenReturn(""); + Mockito.when(right.getQuery()).thenReturn(""); + + Or orCondition = new Or(left, right); + String orQuery = orCondition.getQuery(); + + Assertions.assertEquals("", orQuery); + } + + @Test + public void testOrCondition_whenRightIsNullLeftIsNull_thenReturnEmptyQuery() { + + Mockito.when(left.getQuery()).thenReturn(null); + Mockito.when(right.getQuery()).thenReturn(null); + + Or orCondition = new Or(left, right); + String orQuery = orCondition.getQuery(); + + Assertions.assertEquals("", orQuery); + } +} diff --git a/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java b/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java new file mode 100644 index 0000000..f8c6b30 --- /dev/null +++ b/core/lettucemod-query/src/test/java/com/redis/query/TestOrList.java @@ -0,0 +1,77 @@ +package com.redis.query; + +import com.redis.search.query.filter.Condition; +import com.redis.search.query.filter.OrList; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestOrList { + Condition cond1; + Condition cond2; + Condition cond3; + + @BeforeEach + public void setup() { + cond1 = Mockito.mock(Condition.class); + cond2 = Mockito.mock(Condition.class); + cond3 = Mockito.mock(Condition.class); + } + + @Test + public void testOrCondition_whenConditionsArePresent_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + Mockito.when(cond2.getQuery()).thenReturn("@price:[1000 2000]"); + Mockito.when(cond3.getQuery()).thenReturn("@rating:[4.0 5.0]"); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("((@category:{italian})|(@price:[1000 2000])|(@rating:[4.0 5.0]))", orListQuery); + } + + @Test + public void testOrCondition_whenConditionsAreEmpty_thenReturnEmptyQuery() { + Mockito.when(cond1.getQuery()).thenReturn(""); + Mockito.when(cond2.getQuery()).thenReturn(""); + Mockito.when(cond3.getQuery()).thenReturn(""); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("", orListQuery); + } + + @Test + public void testOrCondition_whenConditionsAreNull_thenReturnEmptyQuery() { + Mockito.when(cond1.getQuery()).thenReturn(null); + Mockito.when(cond2.getQuery()).thenReturn(null); + Mockito.when(cond3.getQuery()).thenReturn(null); + + OrList orListCondition = new OrList(cond1, cond2, cond3); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("", orListQuery); + } + + @Test + public void testOrCondition_whenSingleConditionIsPresent_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + + OrList orListCondition = new OrList(cond1); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})", orListQuery); + } + + @Test + public void testOrCondition_whenSingleConditionIsPresentAlongWithNullCondition_thenReturnQuery() { + Mockito.when(cond1.getQuery()).thenReturn("@category:{italian}"); + + OrList orListCondition = new OrList(cond1, null); + String orListQuery = orListCondition.getQuery(); + + Assertions.assertEquals("(@category:{italian})", orListQuery); + } +} From 7ee8a5c93e584567c523146bd3e5ac74375d5cd5 Mon Sep 17 00:00:00 2001 From: kicktocode Date: Fri, 25 Apr 2025 15:21:24 +0530 Subject: [PATCH 3/3] Fixed backward compatibility issues. --- .../com/redis/search/query/filter/Or.java | 61 +---------------- .../com/redis/query/QueryBuilderTests.java | 2 +- .../src/test/java/com/redis/query/TestOr.java | 67 ------------------- 3 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 core/lettucemod-query/src/test/java/com/redis/query/TestOr.java diff --git a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java index 89ee0f1..8273572 100644 --- a/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java +++ b/core/lettucemod-query/src/main/java/com/redis/search/query/filter/Or.java @@ -1,70 +1,11 @@ package com.redis.search.query.filter; -import java.util.StringJoiner; - -/** - * Represents a logical OR condition between two {@link Condition} objects. - *

- * This class generates a query that joins the left and right conditions using - * the OR operator ('|') in Redis Search query syntax. - *

- * Example output: - *

{@code
- * (@field1:{value1})|(@field2:{value2})
- * }
- */ public class Or extends CompositeCondition { - /** - * The delimiter used for joining two conditions with OR. - */ public static final String DELIMITER = "|"; - /** - * Constructs an {@code Or} condition from two {@link Condition} objects. - * - * @param left The left condition - * @param right The right condition - */ public Or(Condition left, Condition right) { super(DELIMITER, left, right); } - /** - * Builds a logical OR query between two sub-conditions. - *

- * Logic: - * 1. If both left and right queries are present, returns them in the format "(@leftQuery)|(@rightQuery)". - * 2. If only one of them is present, returns that query directly wrapped in parentheses. - * 3. If both are missing or empty, returns an empty string. - * - * @return A string representing the logical query. - */ - @Override - public String getQuery() { - String leftQuery = (left != null) ? left.getQuery() : null; - String rightQuery = (right != null) ? right.getQuery() : null; - - // Treat empty or whitespace strings as null - if (rightQuery != null && rightQuery.trim().isEmpty()) { - rightQuery = null; - } - - boolean hasLeft = leftQuery != null && !leftQuery.isEmpty(); - boolean hasRight = rightQuery != null && !rightQuery.isEmpty(); - - if (hasLeft && hasRight) { - // Both sides present: (@leftQuery)|(@rightQuery) - return String.format("(%s)|(%s)", leftQuery, rightQuery); - } else if (hasLeft) { - // Only left side present: (@leftQuery) - return String.format("(%s)", leftQuery); - } else if (hasRight) { - // Only right side present: (@rightQuery) - return String.format("(%s)", rightQuery); - } else { - // Both sides missing or blank - return ""; - } - } -} +} \ No newline at end of file diff --git a/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java b/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java index fc0924b..5dc68cb 100644 --- a/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java +++ b/core/lettucemod-query/src/test/java/com/redis/query/QueryBuilderTests.java @@ -109,7 +109,7 @@ void testIntersectionBasic() { void testIntersectionNested() { Condition condition = text("name").term("mark").or("dvir").and(numeric("time").between(100, 200)) .and(numeric("created").lt(1000).not()); - assertEquals("@name:((mark)|(dvir)) @time:[100 200] -@created:[-inf (1000]", condition.getQuery()); + assertEquals("@name:(mark|dvir) @time:[100 200] -@created:[-inf (1000]", condition.getQuery()); } @Test diff --git a/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java b/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java deleted file mode 100644 index 8202d3a..0000000 --- a/core/lettucemod-query/src/test/java/com/redis/query/TestOr.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.redis.query; - -import com.redis.search.query.filter.Condition; -import com.redis.search.query.filter.Or; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -public class TestOr { - Condition left; - Condition right; - - @BeforeEach - public void setup() { - left = Mockito.mock(Condition.class); - right = Mockito.mock(Condition.class); - } - - @Test - public void testOrCondition_whenLeftRightIsPresent_thenReturnQuery() { - - Mockito.when(left.getQuery()).thenReturn("@category:{italian}"); - Mockito.when(right.getQuery()).thenReturn("@priceForTwo:[1500 2000]"); - - Or orCondition = new Or(left, right); - String orQuery = orCondition.getQuery(); - - Assertions.assertEquals("(@category:{italian})|(@priceForTwo:[1500 2000])", orQuery); - } - - @Test - public void testOrCondition_whenRightIsNullLeftIsPresent_thenReturnQuery() { - - Mockito.when(left.getQuery()).thenReturn("@category:{italian}"); - Mockito.when(right.getQuery()).thenReturn(" "); - - Or orCondition = new Or(left, right); - String orQuery = orCondition.getQuery(); - - Assertions.assertEquals("(@category:{italian})", orQuery); - } - - @Test - public void testOrCondition_whenRightIsEmptyLeftIsEmpty_thenReturnEmptyQuery() { - - Mockito.when(left.getQuery()).thenReturn(""); - Mockito.when(right.getQuery()).thenReturn(""); - - Or orCondition = new Or(left, right); - String orQuery = orCondition.getQuery(); - - Assertions.assertEquals("", orQuery); - } - - @Test - public void testOrCondition_whenRightIsNullLeftIsNull_thenReturnEmptyQuery() { - - Mockito.when(left.getQuery()).thenReturn(null); - Mockito.when(right.getQuery()).thenReturn(null); - - Or orCondition = new Or(left, right); - String orQuery = orCondition.getQuery(); - - Assertions.assertEquals("", orQuery); - } -}