From 8010ab9e0e6cfc574d5f32f53b50463ecd4c99a3 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Thu, 12 Dec 2024 13:22:02 +0100 Subject: [PATCH] [Fix #2158] Adding more filter conditions --- .../org/kie/kogito/index/json/JsonUtils.java | 7 +- .../graphql/query/GraphQLQueryMapper.java | 12 +++ .../graphql/query/GraphQLQueryMapperTest.java | 32 ++++++++ .../kie/kogito/index/test/QueryTestUtils.java | 5 ++ .../org/kie/kogito/index/test/TestUtils.java | 16 ++++ .../postgresql/PostgresqlJsonNavigator.java | 47 +++++++++++- .../query/ProcessInstanceEntityQueryIT.java | 75 +++++++++++++++++-- .../api/query/QueryFilterFactory.java | 2 +- 8 files changed, 183 insertions(+), 13 deletions(-) diff --git a/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java b/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java index 2db647fb85..e8a7a6d30f 100644 --- a/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java +++ b/data-index/data-index-common/src/main/java/org/kie/kogito/index/json/JsonUtils.java @@ -59,10 +59,11 @@ private static ObjectNode createObjectNode(String variableName, Object variableV } return result; } - + public static AttributeFilter jsonFilter(AttributeFilter filter) { - filter.setJson(true); + if (filter != null) { + filter.setJson(true); + } return filter; } - } diff --git a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java index 4925d6684a..73c3793ec3 100644 --- a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java +++ b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java @@ -150,6 +150,18 @@ private AttributeFilter mapJsonArgument(String attribute, String key, Object return jsonFilter(lessThanEqual(sb.toString(), value)); case BETWEEN: return jsonFilter(filterValueMap(value, val -> between(sb.toString(), val.get("from"), val.get("to")))); + case IN: + return jsonFilter(filterValueList(value, val -> in(sb.toString(), val))); + case IS_NULL: + return jsonFilter(Boolean.TRUE.equals(value) ? isNull(sb.toString()) : notNull(sb.toString())); + case CONTAINS: + return jsonFilter(contains(sb.toString(), value)); + case LIKE: + return jsonFilter(like(sb.toString(), value.toString())); + case CONTAINS_ALL: + return filterValueList(value, val -> containsAll(sb.toString(), val)); + case CONTAINS_ANY: + return filterValueList(value, val -> containsAny(sb.toString(), val)); case EQUAL: default: return jsonFilter(equalTo(sb.toString(), value)); diff --git a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java index 416da2aa37..1d9e3fdbee 100644 --- a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java +++ b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.index.graphql.query; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -71,4 +73,34 @@ void testJsonMapperBetween() { assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("between", Map.of("from", 1, "to", 3)))))).containsExactly( jsonFilter(between("variables.workflowdata.number", 1, 3))); } + + @Test + void testJsonMapperIn() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("in", List.of(1, 3)))))).containsExactly( + jsonFilter(in("variables.workflowdata.number", Arrays.asList(1, 3)))); + } + + @Test + void testJsonMapperContains() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("contains", 1))))).containsExactly( + jsonFilter(contains("variables.workflowdata.number", 1))); + } + + @Test + void testJsonMapperLike() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("like", "kk"))))).containsExactly( + jsonFilter(like("variables.workflowdata.number", "kk"))); + } + + @Test + void testJsonMapperNull() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("isNull", true))))).containsExactly( + jsonFilter(isNull("variables.workflowdata.number"))); + } + + @Test + void testJsonMapperNotNull() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("isNull", false))))).containsExactly( + jsonFilter(notNull("variables.workflowdata.number"))); + } } diff --git a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java index 6477143d26..5e8f305dc8 100644 --- a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java +++ b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/QueryTestUtils.java @@ -50,4 +50,9 @@ public static BiConsumer, String[]> assertWithObjectNodeInOrder public static BiConsumer, String[]> assertWithObjectNode() { return (instances, ids) -> assertThat(instances).hasSize(ids == null ? 0 : ids.length).extracting(n -> n.get("id").asText()).containsExactlyInAnyOrder(ids); } + + public static BiConsumer, String[]> assertNotId() { + return (instances, ids) -> assertThat(instances).extracting("id").doesNotContainAnyElementsOf(List.of(ids)); + } + } diff --git a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java index 59ee7601a0..954bbf4a24 100644 --- a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java +++ b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java @@ -42,7 +42,9 @@ import org.kie.kogito.jackson.utils.ObjectMapperFactory; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; @@ -83,6 +85,20 @@ public static ProcessInstanceVariableDataEvent createProcessInstanceVariableEven return event; } + public static ProcessInstanceVariableDataEvent createProcessInstanceVariableEvent(String processInstance, + String processId, String name, int age, boolean isMartian, List aliases) { + ProcessInstanceVariableDataEvent event = new ProcessInstanceVariableDataEvent(); + event.setKogitoProcessId(processId); + event.setKogitoProcessInstanceId(processInstance); + ArrayNode node = ObjectMapperFactory.get().createArrayNode(); + aliases.forEach(s -> node.add(new TextNode(s))); + event.setData(ProcessInstanceVariableEventBody.create().processId(processId).processInstanceId(processInstance) + .variableName("traveller").variableValue(ObjectMapperFactory.get().createObjectNode().put("name", name).put("age", age).put("isMartian", isMartian) + .set("aliases", node)) + .build()); + return event; + } + public static ProcessInstanceNodeDataEvent createProcessInstanceNodeDataEvent(String processInstance, String processId, String nodeDefinitionId, String nodeInstanceId, String nodeName, String nodeType, int eventType) { diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonNavigator.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonNavigator.java index b40235f612..4878687bc0 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonNavigator.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonNavigator.java @@ -18,6 +18,9 @@ */ package org.kie.kogito.index.postgresql; +import java.util.List; +import java.util.stream.Collectors; + import org.kie.kogito.persistence.api.query.AttributeFilter; import jakarta.persistence.criteria.CriteriaBuilder; @@ -25,6 +28,8 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import static java.util.stream.Collectors.toList; + public class PostgresqlJsonNavigator { private PostgresqlJsonNavigator() { @@ -32,19 +37,54 @@ private PostgresqlJsonNavigator() { public static Predicate buildPredicate(AttributeFilter filter, Root root, CriteriaBuilder builder) { + boolean isString; switch (filter.getCondition()) { case EQUAL: - boolean isString = filter.getValue() instanceof String; + isString = filter.getValue() instanceof String; return builder.equal(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case BETWEEN: + isString = filter.getValue() instanceof String; + List value = (List) filter.getValue(); + return builder.between(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, value.get(0), isString), + buildObjectExpression(builder, value.get(1), isString)); + case GT: + isString = filter.getValue() instanceof String; + return builder.greaterThan(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case GTE: + isString = filter.getValue() instanceof String; + return builder.greaterThanOrEqualTo(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case LT: + isString = filter.getValue() instanceof String; + return builder.lessThan(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case LTE: + isString = filter.getValue() instanceof String; + return builder + .lessThanOrEqualTo(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString)); + case IN: + List values = ((List) filter.getValue()); + isString = values.get(0) instanceof String; + return buildPathExpression(builder, root, filter.getAttribute(), isString).in(values.stream().map(o -> buildObjectExpression(builder, o, isString)).collect(Collectors.toList())); + case LIKE: + isString = filter.getValue() instanceof String; + return builder.like(buildPathExpression(builder, root, filter.getAttribute(), isString), + filter.getValue().toString().replaceAll("\\*", "%")); + case IS_NULL: + return builder.isNull(buildPathExpression(builder, root, filter.getAttribute(), false)); + case NOT_NULL: + return builder.isNotNull(buildPathExpression(builder, root, filter.getAttribute(), false)); } throw new UnsupportedOperationException(); } - private static Expression buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) { + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) { return isString ? builder.literal(value) : builder.function("to_jsonb", Object.class, builder.literal(value)); } - private static Expression buildPathExpression(CriteriaBuilder builder, Root root, String attributeName, boolean isStr) { + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value) { + return buildObjectExpression(builder, value, value instanceof String); + } + + private static Expression buildPathExpression(CriteriaBuilder builder, Root root, String attributeName, boolean isStr) { String[] attributes = attributeName.split("\\."); Expression[] arguments = new Expression[attributes.length]; arguments[0] = root.get(attributes[0]); @@ -53,5 +93,4 @@ private static Expression buildPathExpression(CriteriaBuilder builder, Root assertThat(instances).isEmpty(), storage, singletonList(jsonFilter(equalTo("variables.traveller.firstName", "Smith"))), null, null, null, + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.name", "Smith"))), null, null, null, processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.isMartian", false))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.isMartian", true))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(equalTo("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 26, 30))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 1, 3))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 26, 30))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(between("variables.traveller.age", 1, 3))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(greaterThan("variables.traveller.age", 26))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(greaterThan("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(greaterThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(greaterThanEqual("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThan("variables.traveller.age", 29))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThan("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 27))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 28))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(lessThanEqual("variables.traveller.age", 27))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(in("variables.traveller.name", List.of("John", "Smith")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of("Jack", "Smith")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of(28, 29)))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(in("variables.traveller.age", List.of(27, 29)))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(like("variables.traveller.name", "Joh*"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(like("variables.traveller.name", "Joha*"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(notNull("variables.traveller.aliases"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(isNull("variables.traveller.aliases"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(not(jsonFilter(isNull("variables.traveller.aliases")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(and(List.of(jsonFilter(notNull("variables.traveller.aliases")), jsonFilter(lessThan("variables.traveller.age", 45))))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(or(List.of(jsonFilter(notNull("variables.traveller.aliases")), jsonFilter(lessThan("variables.traveller.age", 22))))), null, null, null, + processInstanceId); + // TODO add support for json contains (requires writing dialect extension on hibernate) + //queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheRealThing"))), null, null, null, + // processInstanceId); + //queryAndAssert(assertEmpty(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheDummyThing"))), null, null, null, + // processInstanceId); } } diff --git a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java index a54f58919a..ab02f57810 100644 --- a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java +++ b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/QueryFilterFactory.java @@ -34,7 +34,7 @@ public static AttributeFilter like(String attribute, String value) { return new AttributeFilter<>(attribute, FilterCondition.LIKE, value); } - public static AttributeFilter contains(String attribute, String value) { + public static AttributeFilter contains(String attribute, T value) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS, value); }