Fix ghost joins in rsql with maps (#3088)

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2026-06-12 10:39:55 +03:00
committed by GitHub
parent fc2e1a2f69
commit 9cb8a7cf00
3 changed files with 32 additions and 17 deletions

View File

@@ -155,10 +155,9 @@ public class SpecificationBuilder<T> {
String.format("Operator %s is not supported for map fields with value null", op)); String.format("Operator %s is not supported for map fields with value null", op));
}; };
} else { } else {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) pathResolver.getPath(attribute); // map entry with key not null (exist) - use left join per key, key/value filtered in where
return isNot(op) final MapJoin<?, ?, ?> mapJoin = pathResolver.getJoinForWhere(attribute, split[1]);
? compare(comparison, toMapValuePath(pathResolver.getJoinOnInner(attribute, split[1]))) return cb.and(equal(mapJoin.key(), split[1]), compare(comparison, toMapValuePath(mapJoin)));
: cb.and(equal(mapPath.key(), split[1]), compare(comparison, toMapValuePath(mapPath)));
} }
} else if (attribute instanceof SetAttribute<?, ?> setAttribute) { } else if (attribute instanceof SetAttribute<?, ?> setAttribute) {
if (split.length < 2 || ObjectUtils.isEmpty(split[1])) { if (split.length < 2 || ObjectUtils.isEmpty(split[1])) {
@@ -437,8 +436,8 @@ public class SpecificationBuilder<T> {
return getCollectionPathResolver(attribute.getName()).getJoinOn(value); return getCollectionPathResolver(attribute.getName()).getJoinOn(value);
} }
private MapJoin<?, ?, ?> getJoinOnInner(final Attribute<?, ?> attribute, final Object value) { private MapJoin<?, ?, ?> getJoinForWhere(final Attribute<?, ?> attribute, final Object mapKeyName) {
return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value); return getCollectionPathResolver(attribute.getName()).getJoinForWhere(mapKeyName);
} }
private Map<String, Integer> getState() { private Map<String, Integer> getState() {
@@ -463,7 +462,7 @@ public class SpecificationBuilder<T> {
@Setter @Setter
private int pos; private int pos;
private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>(); private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>();
private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>(); private final Map<Object, MapJoin<?, ?, ?>> joinForWhereCache = new HashMap<>();
private CollectionPathResolver(final String attributeName) { private CollectionPathResolver(final String attributeName) {
this.attributeName = attributeName; this.attributeName = attributeName;
@@ -488,12 +487,9 @@ public class SpecificationBuilder<T> {
}); });
} }
private MapJoin<?, ?, ?> getJoinOnInner(final Object value) { private MapJoin<?, ?, ?> getJoinForWhere(final Object mapKeyName) {
return joinOnInnerCache.computeIfAbsent(value, k -> { return joinForWhereCache.computeIfAbsent(mapKeyName, k ->
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.INNER); (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.LEFT));
mapPath.on(equal(mapPath.key(), k));
return mapPath;
});
} }
} }
} }

View File

@@ -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==rooty")).hasSize(1).containsExactlyInAnyOrder(root1);
assertThat(filter("subMap.x==*tx and subMap.y!=rootx")).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")) assertThat(filter("subMap.x==*tx or subMap.x==rooty")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
.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!=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 @Test

View File

@@ -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)"); "((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, 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)"); "((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 @Test