From ca6a92969488403076b3c0ce5ae7b767b2eb7a7b Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:53:38 +0100 Subject: [PATCH] [Fix #2163] Add contains, containsAny and contaisAll support for variables field (#2164) * [Fix #2163] Add contains support * [Fix #2163] ContainsAny&ConstainsAll --- .../graphql/query/GraphQLQueryMapper.java | 4 +- .../graphql/query/GraphQLQueryMapperTest.java | 12 +++ .../index/postgresql/ContainsSQLFunction.java | 76 +++++++++++++++++++ .../CustomFunctionsContributor.java | 36 +++++++++ .../postgresql/PostgresqlJsonHelper.java | 15 ++++ ...g.hibernate.boot.model.FunctionContributor | 19 +++++ .../query/ProcessInstanceEntityQueryIT.java | 17 +++-- .../api/query/QueryFilterFactory.java | 4 +- 8 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java create mode 100644 data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor 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 73c3793ec3..1fb357ffa0 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 @@ -159,9 +159,9 @@ private AttributeFilter mapJsonArgument(String attribute, String key, Object case LIKE: return jsonFilter(like(sb.toString(), value.toString())); case CONTAINS_ALL: - return filterValueList(value, val -> containsAll(sb.toString(), val)); + return jsonFilter(filterValueList(value, val -> containsAll(sb.toString(), val))); case CONTAINS_ANY: - return filterValueList(value, val -> containsAny(sb.toString(), val)); + return jsonFilter(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 1d9e3fdbee..27f053f9a3 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 @@ -86,6 +86,18 @@ void testJsonMapperContains() { jsonFilter(contains("variables.workflowdata.number", 1))); } + @Test + void testJsonMapperContainsAny() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("containsAny", List.of(1, 2, 3)))))).containsExactly( + jsonFilter(containsAny("variables.workflowdata.number", List.of(1, 2, 3)))); + } + + @Test + void testJsonMapperContainsAll() { + assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("containsAll", List.of(1, 2, 3)))))).containsExactly( + jsonFilter(containsAll("variables.workflowdata.number", List.of(1, 2, 3)))); + } + @Test void testJsonMapperLike() { assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", Map.of("number", Map.of("like", "kk"))))).containsExactly( diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java new file mode 100644 index 0000000000..ef0563b936 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/ContainsSQLFunction.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import java.util.Iterator; +import java.util.List; + +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.SqlTypes; + +public class ContainsSQLFunction extends StandardSQLFunction { + + static final String CONTAINS_NAME = "contains"; + static final String CONTAINS_ALL_NAME = "containsAll"; + static final String CONTAINS_ANY_NAME = "containsAny"; + + static final String CONTAINS_SEQ = "??"; + static final String CONTAINS_ALL_SEQ = "??&"; + static final String CONTAINS_ANY_SEQ = "??|"; + + private final String operator; + + private static final BasicTypeReference RETURN_TYPE = new BasicTypeReference<>("boolean", Boolean.class, SqlTypes.BOOLEAN); + + ContainsSQLFunction(String name, String operator) { + super(name, RETURN_TYPE); + this.operator = operator; + } + + @Override + public void render( + SqlAppender sqlAppender, + List args, + ReturnableType returnType, + SqlAstTranslator translator) { + int size = args.size(); + if (size < 2) { + throw new IllegalArgumentException("Function " + getName() + " requires at least two arguments"); + } + Iterator iter = args.iterator(); + iter.next().accept(translator); + sqlAppender.append(' '); + sqlAppender.append(operator); + sqlAppender.append(' '); + if (size == 2) { + iter.next().accept(translator); + } else { + sqlAppender.append("array["); + do { + iter.next().accept(translator); + sqlAppender.append(iter.hasNext() ? ',' : ']'); + } while (iter.hasNext()); + } + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java new file mode 100644 index 0000000000..cb8a68c9e1 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/CustomFunctionsContributor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.kie.kogito.index.postgresql; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.FunctionContributor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; + +import static org.kie.kogito.index.postgresql.ContainsSQLFunction.*; + +public class CustomFunctionsContributor implements FunctionContributor { + + @Override + public void contributeFunctions(FunctionContributions functionContributions) { + SqmFunctionRegistry registry = functionContributions.getFunctionRegistry(); + registry.register(CONTAINS_NAME, new ContainsSQLFunction(CONTAINS_NAME, CONTAINS_SEQ)); + registry.register(CONTAINS_ANY_NAME, new ContainsSQLFunction(CONTAINS_ANY_NAME, CONTAINS_ANY_SEQ)); + registry.register(CONTAINS_ALL_NAME, new ContainsSQLFunction(CONTAINS_ALL_NAME, CONTAINS_ALL_SEQ)); + } +} diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java index 6ca18f3567..3cb1f347d8 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/PostgresqlJsonHelper.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.kie.kogito.persistence.api.query.AttributeFilter; @@ -72,10 +73,24 @@ public static Predicate buildPredicate(AttributeFilter filter, Root root, 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 CONTAINS: + return builder.isTrue( + builder.function(ContainsSQLFunction.CONTAINS_NAME, Boolean.class, buildPathExpression(builder, root, filter.getAttribute(), false), builder.literal(filter.getValue()))); + case CONTAINS_ANY: + return containsPredicate(filter, root, builder, ContainsSQLFunction.CONTAINS_ANY_NAME); + case CONTAINS_ALL: + return containsPredicate(filter, root, builder, ContainsSQLFunction.CONTAINS_ALL_NAME); } throw new UnsupportedOperationException("Filter " + filter + " is not supported"); } + private static Predicate containsPredicate(AttributeFilter filter, Root root, CriteriaBuilder builder, String name) { + return builder.isTrue( + builder.function(name, Boolean.class, + Stream.concat(Stream.of(buildPathExpression(builder, root, filter.getAttribute(), false)), ((List) filter.getValue()).stream().map(o -> builder.literal(o))) + .toArray(Expression[]::new))); + } + private static Expression buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) { return isString ? builder.literal(value) : builder.function("to_jsonb", Object.class, builder.literal(value)); } diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor b/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor new file mode 100644 index 0000000000..c3c15eeb39 --- /dev/null +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +org.kie.kogito.index.postgresql.CustomFunctionsContributor \ No newline at end of file diff --git a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java index 872ba48906..9eb7611e74 100644 --- a/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java +++ b/data-index/data-index-storage/data-index-storage-postgresql/src/test/java/org/kie/kogito/index/postgresql/query/ProcessInstanceEntityQueryIT.java @@ -111,10 +111,17 @@ void testProcessInstanceVariables() { 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); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheRealThing"))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(contains("variables.traveller.aliases", "TheDummyThing"))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(containsAny("variables.traveller.aliases", List.of("TheRealThing", "TheDummyThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(containsAny("variables.traveller.aliases", List.of("TheRedPandaThing", "TheDummyThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertWithId(), storage, singletonList(jsonFilter(containsAll("variables.traveller.aliases", List.of("Super", "Astonishing", "TheRealThing")))), null, null, null, + processInstanceId); + queryAndAssert(assertNotId(), storage, singletonList(jsonFilter(containsAll("variables.traveller.aliases", List.of("Super", "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 ab02f57810..1017556895 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 @@ -42,11 +42,11 @@ public static AttributeFilter> in(String attribute, List values) return new AttributeFilter<>(attribute, FilterCondition.IN, values); } - public static AttributeFilter> containsAny(String attribute, List values) { + public static AttributeFilter> containsAny(String attribute, List values) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS_ANY, values); } - public static AttributeFilter> containsAll(String attribute, List values) { + public static AttributeFilter> containsAll(String attribute, List values) { return new AttributeFilter<>(attribute, FilterCondition.CONTAINS_ALL, values); }