Fix G3 - deep attributes (#2462)

This commit is contained in:
Avgustin Marinov
2025-06-17 15:44:55 +03:00
committed by GitHub
parent 62b1b7d730
commit d80e1e004d
12 changed files with 296 additions and 34 deletions

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.assertj.core.api.Assertions.assertThat;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.eclipse.hawkbit.repository.RolloutFields;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition;
import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@Feature("Component Tests - Repository")
@Story("RSQL filter rollout group")
class RSQLRolloutFieldTest extends AbstractJpaIntegrationTest {
private Rollout rollout;
@BeforeEach
void setupBeforeTest() {
testdataFactory.createTargets(20, "rollout", "rollout");
final DistributionSet dsA = testdataFactory.createDistributionSet("");
rollout = createRollout("rollout1", 4, dsA.getId(), "controllerId==rollout*");
rollout = rolloutManagement.get(rollout.getId()).get();
}
@Test
@Description("Test filter rollout by distrbution set type id")
void testFilterByDsType() {
assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "==" + rollout.getDistributionSet().getType().getId() + 1, 0);
assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "!=" + rollout.getDistributionSet().getType().getId() + 1, 1);
assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "==" + rollout.getDistributionSet().getType().getId(), 1);
assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "!=" + rollout.getDistributionSet().getType().getId(), 0);
}
private void assertRSQLQuery(final String rsql, final long expectedTargets) {
final Page<Rollout> findTargetPage = rolloutManagement.findByRsql(rsql, false, PageRequest.of(0, 100));
final long countTargetsAll = findTargetPage.getTotalElements();
assertThat(findTargetPage).isNotNull();
assertThat(countTargetsAll).isEqualTo(expectedTargets);
}
private Rollout createRollout(final String name, final int amountGroups, final long distributionSetId, final String targetFilterQuery) {
return rolloutManagement.create(
entityFactory.rollout().create().distributionSetId(
distributionSetManagement.get(distributionSetId).get()).name(name).targetFilterQuery(targetFilterQuery),
amountGroups,
false,
new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build());
}
}

View File

@@ -107,9 +107,22 @@ class ReferenceMatcher {
if (split.length == 1) {
return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType()));
} else {
final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]);
return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op,
map(comparison.getValue(), valueGetter.getReturnType()));
if (split[1].contains(".")) {
// nested field access
final String[] nestedSplit = split[1].split("\\.", 2);
final Method nestedFieldGetter = getGetter(fieldGetter.getReturnType(), nestedSplit[0]);
nestedFieldGetter.setAccessible(true);
final Method valueGetter = getGetter(nestedFieldGetter.getReturnType(), nestedSplit[1]);
final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.invoke(fieldValue);
return compare(
nestedFieldValue == null ? null : valueGetter.invoke(nestedFieldValue),
op,
map(comparison.getValue(), valueGetter.getReturnType()));
} else {
final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]);
return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op,
map(comparison.getValue(), valueGetter.getReturnType()));
}
}
}
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {

View File

@@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RootRepository
extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}
public interface RootRepository extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}

View File

@@ -88,6 +88,16 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
runWithRsqlToSpecBuilder(super::pluralSubMapAttribute, LEGACY_G2);
}
@Test
void singularEntitySubSubAttributeG1() {
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G1);
}
@Override
@Test
void singularEntitySubSubAttribute() {
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G2);
}
@Override
protected Specification<Root> getSpecification(final String rsql) {
return builder.specification(rsql);
@@ -98,7 +108,7 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
INTVALUE("intValue"),
STRVALUE("strValue"),
SUBENTITY("subEntity", "strValue", "intValue"),
SUBENTITY("subEntity", "strValue", "intValue", "subSub"),
SUBSET("subSet", "strValue", "intValue"),
SUBMAP("subMap");

View File

@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import java.util.List;
import java.util.Map;
@@ -43,6 +44,8 @@ import org.springframework.orm.jpa.vendor.Database;
@Slf4j
class SpecificationBuilderTest {
@Autowired
private SubSubRepository subSubRepository;
@Autowired
private SubRepository subRepository;
@Autowired
@@ -214,7 +217,8 @@ class SpecificationBuilderTest {
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==1")).isEmpty();
assertThat(filter("subEntity.strValue==subx and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==subx or subEntity.intValue==1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue==subx or subEntity.intValue==1")).hasSize(4)
.containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.strValue==subx or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
@@ -365,6 +369,106 @@ class SpecificationBuilderTest {
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
}
@Test
void singularEntitySubSubAttribute() {
final SubSub subSub1 = subSubRepository.save(new SubSub().setStrValue("subx").setIntValue(0));
final SubSub subSub2 = subSubRepository.save(new SubSub().setStrValue("suby").setIntValue(1));
final Sub sub1 = subRepository.save(new Sub().setSubSub(subSub1));
final Sub sub2 = subRepository.save(new Sub().setSubSub(subSub2));
final Root root1 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root2 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root3 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root4 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root5 = rootRepository.save(new Root()); // no sub set
// by sub entity string
assertThat(filter("subEntity.subSub.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==nostr")).isEmpty();
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it is
assertThat(filter("subEntity.subSub.strValue!=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue!=nostr")).hasSize(5);
assertThat(filter("subEntity.subSub.strValue<suby")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue<=suby")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue>subx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.subSub.strValue>=subx")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue=in=subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue=in=(subx, suby)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue=in=(subZ, subT)")).isEmpty();
assertThat(filter("subEntity.subSub.strValue=out=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue=out=(subx, suby)")).hasSize(1).containsExactlyInAnyOrder(root5);
// wildcard, like
assertThat(filter("subEntity.subSub.strValue==sub*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue==*bx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.subSub.strValue!=*bx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// null checks
if (getRsqlToSpecBuilder() != LEGACY_G2) {
assertThat(filter("subEntity.subSub.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
}
assertThat(filter("subEntity.subSub.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
}
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==suby")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue!=subx")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==subx"))
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue==suby"))
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not, and doesn't support =is= and =not=
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue!=subx"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue=is=null"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
// by sub entity int
assertThat(filter("subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==2")).isEmpty();
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.intValue!=2")).hasSize(5);
}
assertThat(filter("subEntity.subSub.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.subSub.intValue>=0")).hasSize(4);
assertThat(filter("subEntity.subSub.intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.intValue=in=(2, 3)")).isEmpty();
assertThat(filter("subEntity.subSub.intValue=out=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==1")).isEmpty();
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
.containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue!=1"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==1")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue!=1")).hasSize(2)
.containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==0"))
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue==1"))
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue!=1"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
}
private List<Root> filter(final String rsql) {
// reference / auto filter (using elements and reflection)
final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql);

View File

@@ -10,9 +10,13 @@
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -30,4 +34,7 @@ class Sub {
// basic
private String strValue;
private int intValue;
// entity
@ManyToOne
private SubSub subSub;
}

View File

@@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SubRepository
extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}
public interface SubRepository extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.experimental.Accessors;
@Entity
@Data
@Accessors(chain = true)
class SubSub {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// singular attributes
// basic
private String strValue;
private int intValue;
}

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SubSubRepository extends CrudRepository<SubSub, Long>, JpaSpecificationExecutor<SubSub> {}