diff --git a/hawkbit-ql-jpa/src/main/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilder.java b/hawkbit-ql-jpa/src/main/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilder.java index 726ef822c..d12d687f8 100644 --- a/hawkbit-ql-jpa/src/main/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilder.java +++ b/hawkbit-ql-jpa/src/main/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilder.java @@ -155,10 +155,9 @@ public class SpecificationBuilder { String.format("Operator %s is not supported for map fields with value null", op)); }; } else { - final MapJoin mapPath = (MapJoin) pathResolver.getPath(attribute); - return isNot(op) - ? compare(comparison, toMapValuePath(pathResolver.getJoinOnInner(attribute, split[1]))) - : cb.and(equal(mapPath.key(), split[1]), compare(comparison, toMapValuePath(mapPath))); + // map entry with key not null (exist) - use left join per key, key/value filtered in where + final MapJoin mapJoin = pathResolver.getJoinForWhere(attribute, split[1]); + return cb.and(equal(mapJoin.key(), split[1]), compare(comparison, toMapValuePath(mapJoin))); } } else if (attribute instanceof SetAttribute setAttribute) { if (split.length < 2 || ObjectUtils.isEmpty(split[1])) { @@ -437,8 +436,8 @@ public class SpecificationBuilder { return getCollectionPathResolver(attribute.getName()).getJoinOn(value); } - private MapJoin getJoinOnInner(final Attribute attribute, final Object value) { - return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value); + private MapJoin getJoinForWhere(final Attribute attribute, final Object mapKeyName) { + return getCollectionPathResolver(attribute.getName()).getJoinForWhere(mapKeyName); } private Map getState() { @@ -463,7 +462,7 @@ public class SpecificationBuilder { @Setter private int pos; private final Map> joinOnCache = new HashMap<>(); - private final Map> joinOnInnerCache = new HashMap<>(); + private final Map> joinForWhereCache = new HashMap<>(); private CollectionPathResolver(final String attributeName) { this.attributeName = attributeName; @@ -488,12 +487,9 @@ public class SpecificationBuilder { }); } - private MapJoin getJoinOnInner(final Object value) { - return joinOnInnerCache.computeIfAbsent(value, k -> { - final MapJoin mapPath = (MapJoin) root.join(attributeName, JoinType.INNER); - mapPath.on(equal(mapPath.key(), k)); - return mapPath; - }); + private MapJoin getJoinForWhere(final Object mapKeyName) { + return joinForWhereCache.computeIfAbsent(mapKeyName, k -> + (MapJoin) root.join(attributeName, JoinType.LEFT)); } } } diff --git a/hawkbit-ql-jpa/src/test/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilderTest.java b/hawkbit-ql-jpa/src/test/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilderTest.java index 619ea3c7b..9499c318f 100644 --- a/hawkbit-ql-jpa/src/test/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilderTest.java +++ b/hawkbit-ql-jpa/src/test/java/org/eclipse/hawkbit/ql/jpa/SpecificationBuilderTest.java @@ -308,10 +308,25 @@ class SpecificationBuilderTest { assertThat(filter("subMap.x==*tx and subMap.y==rooty")).hasSize(1).containsExactlyInAnyOrder(root1); assertThat(filter("subMap.x==*tx and subMap.y!=rootx")).hasSize(1).containsExactlyInAnyOrder(root1); - assertThat(filter("subMap.x==*tx or subMap.x==rooty")) - .hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5); - assertThat(filter("subMap.x==*tx or subMap.x!=rootx")) - .hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5); + assertThat(filter("subMap.x==*tx or subMap.x==rooty")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5); + assertThat(filter("subMap.x==*tx or subMap.x!=rootx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5); + + assertThat(filter("subMap.x=out=rootx and subMap.y=out=rooty")).hasSize(1).containsExactlyInAnyOrder(root4); + assertThat(filter("subMap.x!=rootx and subMap.y!=rooty")).hasSize(1).containsExactlyInAnyOrder(root4); + assertThat(filter("subMap.x=out=(rootx, rooty) and subMap.y=out=(rootx, rooty)")).isEmpty(); + assertThat(filter("subMap.x=out=rootx and subMap.y=out=rooty and subMap.x=out=rooty")).isEmpty(); + assertThat(filter("subMap.x==rooty and subMap.y=out=rooty")).hasSize(1).containsExactlyInAnyOrder(root4); + + assertThat(filter("subMap.x==rootx and subMap.y==rooty")).hasSize(1).containsExactlyInAnyOrder(root1); + assertThat(filter("subMap.x==rootx and subMap.y=in=(rooty, rootz)")).hasSize(1).containsExactlyInAnyOrder(root1); + assertThat(filter("subMap.x==rootx and subMap.y==rooty and subMap.x==rooty")).isEmpty(); + assertThat(filter("subMap.x==rooty and subMap.y==rootx")).hasSize(1).containsExactlyInAnyOrder(root4); + + assertThat(filter("subMap.x=in=(rootx) and subMap.y=in=(rooty)")).hasSize(1).containsExactlyInAnyOrder(root1); + assertThat(filter("subMap.x=in=(rootx,rooty) and subMap.y=in=(rootx,rooty)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4); + assertThat(filter("subMap.x=in=(rootx) and subMap.y=in=(rooty) and subMap.x=in=(rooty)")).isEmpty(); + assertThat(filter("subMap.x=out=rooty and subMap.y=out=rootx")).hasSize(1).containsExactlyInAnyOrder(root1); + assertThat(filter("subMap.x=out=rooty and subMap.y!=rootx")).hasSize(1).containsExactlyInAnyOrder(root1); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/ql/rsql/RsqlToSqlTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/ql/rsql/RsqlToSqlTest.java index 0803e53f7..61e1e2e5a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/ql/rsql/RsqlToSqlTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/ql/rsql/RsqlToSqlTest.java @@ -114,6 +114,10 @@ class RsqlToSqlTest { "((attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==03 and updateStatus!=pending)"); print(JpaTarget.class, TargetFields.class, "((attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==01 and updateStatus!=pending)"); + print(JpaTarget.class, TargetFields.class, "attribute.sw_1_version=in=('test1','test2') and attribute.sw_2_version=in=('test3','test4') and attribute.sw_3_version=in=('test5','test6')"); + print(JpaTarget.class, TargetFields.class, "attribute.sw_1_version=out=('test1','test2') and attribute.sw_2_version=out=('test3','test4') and attribute.sw_3_version=out=('test5','test6')"); + print(JpaTarget.class, TargetFields.class, "metadata.key1=in=(value1,value2) and metadata.key2=out=(value3,value4) and metadata.key3=out=(value3,value4)"); + print(JpaTarget.class, TargetFields.class, "metadata.key1=out=(value1,value2) and metadata.key2=out=(value3,value4) and metadata.key3=out=(value3,value4)"); } @Test