Skip to content

Commit d957a50

Browse files
authored
feat: filter state by string (#4829)
1 parent 031aaa3 commit d957a50

File tree

30 files changed

+498
-155
lines changed

30 files changed

+498
-155
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.sql.translation;
16+
17+
import org.eclipse.edc.spi.entity.StateResolver;
18+
import org.eclipse.edc.spi.query.Criterion;
19+
20+
import java.util.Collection;
21+
22+
import static java.util.stream.Collectors.toList;
23+
24+
/**
25+
* Supports the string representation of a state, that will be converted into int by the {@link #stateResolver}.
26+
*/
27+
public class EntityStateFieldTranslator extends PlainColumnFieldTranslator {
28+
29+
private final StateResolver stateResolver;
30+
31+
public EntityStateFieldTranslator(String columnName, StateResolver stateResolver) {
32+
super(columnName);
33+
this.stateResolver = stateResolver;
34+
}
35+
36+
@Override
37+
public Collection<Object> toParameters(Criterion criterion) {
38+
return super.toParameters(criterion)
39+
.stream()
40+
.map(it -> it instanceof String stringState ? stateResolver.resolve(stringState) : it)
41+
.collect(toList());
42+
}
43+
}

core/common/lib/sql-lib/src/main/java/org/eclipse/edc/sql/translation/FieldTranslator.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,7 @@ public interface FieldTranslator {
5050
*/
5151
WhereClause toWhereClause(List<PathItem> path, Criterion criterion, SqlOperator operator);
5252

53-
static String toValuePlaceholder(Criterion criterion) {
54-
if (criterion.getOperandRight() instanceof Collection<?> collection) {
55-
return format("(%s)", String.join(",", nCopies(collection.size(), PREPARED_STATEMENT_PLACEHOLDER)));
56-
}
57-
return PREPARED_STATEMENT_PLACEHOLDER;
58-
}
59-
60-
static Collection<Object> toParameters(Criterion criterion) {
53+
default Collection<Object> toParameters(Criterion criterion) {
6154
var operandRight = criterion.getOperandRight();
6255
if (operandRight == null) {
6356
return emptyList();
@@ -68,4 +61,11 @@ static Collection<Object> toParameters(Criterion criterion) {
6861
}
6962
}
7063

64+
static String toValuePlaceholder(Criterion criterion) {
65+
if (criterion.getOperandRight() instanceof Collection<?> collection) {
66+
return format("(%s)", String.join(",", nCopies(collection.size(), PREPARED_STATEMENT_PLACEHOLDER)));
67+
}
68+
return PREPARED_STATEMENT_PLACEHOLDER;
69+
}
70+
7171
}

core/common/lib/sql-lib/src/main/java/org/eclipse/edc/sql/translation/JsonArrayTranslator.java

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.Collection;
2121
import java.util.List;
2222

23-
import static org.eclipse.edc.sql.translation.FieldTranslator.toParameters;
2423
import static org.eclipse.edc.sql.translation.FieldTranslator.toValuePlaceholder;
2524

2625
/**

core/common/lib/sql-lib/src/main/java/org/eclipse/edc/sql/translation/JsonFieldTranslator.java

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import static java.lang.String.format;
2424
import static java.util.stream.IntStream.range;
25-
import static org.eclipse.edc.sql.translation.FieldTranslator.toParameters;
2625
import static org.eclipse.edc.sql.translation.FieldTranslator.toValuePlaceholder;
2726

2827
public class JsonFieldTranslator implements FieldTranslator {

core/common/lib/sql-lib/src/main/java/org/eclipse/edc/sql/translation/PlainColumnFieldTranslator.java

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.util.List;
2121

22-
import static org.eclipse.edc.sql.translation.FieldTranslator.toParameters;
2322
import static org.eclipse.edc.sql.translation.FieldTranslator.toValuePlaceholder;
2423

2524
public class PlainColumnFieldTranslator implements FieldTranslator {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.sql.translation;
16+
17+
import org.eclipse.edc.spi.entity.StateResolver;
18+
import org.eclipse.edc.util.reflection.PathItem;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.util.List;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
import static org.eclipse.edc.spi.query.Criterion.criterion;
26+
import static org.mockito.ArgumentMatchers.any;
27+
import static org.mockito.Mockito.mock;
28+
import static org.mockito.Mockito.verify;
29+
import static org.mockito.Mockito.when;
30+
31+
class EntityStateFieldTranslatorTest {
32+
33+
private final StateResolver stateResolver = mock();
34+
private final EntityStateFieldTranslator translator = new EntityStateFieldTranslator("column_name", stateResolver);
35+
36+
@Test
37+
void shouldReturnWhereClause_whenOperatorSpecified() {
38+
var path = PathItem.parse("any");
39+
40+
var result = translator.toWhereClause(path, criterion("field", "=", 100), createOperator());
41+
42+
assertThat(result.sql()).isEqualTo("column_name any ?");
43+
assertThat(result.parameters()).containsExactly(100);
44+
}
45+
46+
@Test
47+
void shouldMapMultipleParameters_whenRightOperandIsCollection() {
48+
var path = PathItem.parse("any");
49+
50+
var result = translator.toWhereClause(path, criterion("field", "in", List.of(100, 200)), createOperator());
51+
52+
assertThat(result.sql()).isEqualTo("column_name any (?,?)");
53+
assertThat(result.parameters()).containsExactly(100, 200);
54+
}
55+
56+
@Test
57+
void shouldTranslateStateToCode_whenInputTypeIsString() {
58+
when(stateResolver.resolve(any())).thenReturn(100);
59+
var path = PathItem.parse("any");
60+
61+
var result = translator.toWhereClause(path, criterion("field", "=", "STATE"), createOperator());
62+
63+
assertThat(result.sql()).isEqualTo("column_name any ?");
64+
assertThat(result.parameters()).containsExactly(100);
65+
verify(stateResolver).resolve("STATE");
66+
}
67+
68+
@Test
69+
void shouldMapMultipleParametersToCode_whenRightOperandIsStringCollection() {
70+
when(stateResolver.resolve(any())).thenReturn(100).thenReturn(200);
71+
var path = PathItem.parse("any");
72+
73+
var result = translator.toWhereClause(path, criterion("field", "in", List.of("STATE", "ANOTHER_STATE")), createOperator());
74+
75+
assertThat(result.sql()).isEqualTo("column_name any (?,?)");
76+
assertThat(result.parameters()).containsExactly(100, 200);
77+
verify(stateResolver).resolve("STATE");
78+
verify(stateResolver).resolve("ANOTHER_STATE");
79+
}
80+
81+
@NotNull
82+
private static SqlOperator createOperator() {
83+
return new SqlOperator("any", Object.class);
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.store;
16+
17+
import org.eclipse.edc.spi.query.CriteriaToPredicate;
18+
import org.eclipse.edc.spi.query.Criterion;
19+
import org.eclipse.edc.spi.query.CriterionOperatorRegistry;
20+
21+
import java.util.List;
22+
import java.util.function.Predicate;
23+
24+
/**
25+
* Converts criteria to a Predicate that converts all the criterion to predicate using the passed {@link #criterionOperatorRegistry}
26+
* and joins them with "and" operator.
27+
*
28+
* @param <T> the object type
29+
*/
30+
public class AndOperatorCriteriaToPredicate<T> implements CriteriaToPredicate<T> {
31+
32+
private final CriterionOperatorRegistry criterionOperatorRegistry;
33+
34+
public AndOperatorCriteriaToPredicate(CriterionOperatorRegistry criterionOperatorRegistry) {
35+
this.criterionOperatorRegistry = criterionOperatorRegistry;
36+
}
37+
38+
@Override
39+
public Predicate<T> convert(List<Criterion> criteria) {
40+
return criteria.stream()
41+
.map(criterionOperatorRegistry::<T>toPredicate)
42+
.reduce(x -> true, Predicate::and);
43+
}
44+
}

core/common/lib/store-lib/src/main/java/org/eclipse/edc/store/InMemoryStatefulEntityStore.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package org.eclipse.edc.store;
1616

17+
import org.eclipse.edc.spi.entity.StateResolver;
1718
import org.eclipse.edc.spi.entity.StatefulEntity;
1819
import org.eclipse.edc.spi.persistence.Lease;
1920
import org.eclipse.edc.spi.persistence.StateEntityStore;
@@ -55,8 +56,8 @@ public class InMemoryStatefulEntityStore<T extends StatefulEntity<T>> implements
5556
private final Map<String, Lease> leases = new HashMap<>();
5657
protected final CriterionOperatorRegistry criterionOperatorRegistry;
5758

58-
public InMemoryStatefulEntityStore(Class<T> clazz, String lockId, Clock clock, CriterionOperatorRegistry criterionOperatorRegistry) {
59-
queryResolver = new ReflectionBasedQueryResolver<>(clazz, criterionOperatorRegistry);
59+
public InMemoryStatefulEntityStore(Class<T> clazz, String lockId, Clock clock, CriterionOperatorRegistry criterionOperatorRegistry, StateResolver stateResolver) {
60+
this.queryResolver = new ReflectionBasedQueryResolver<>(clazz, new StatefulEntityCriteriaToPredicate<>(criterionOperatorRegistry, stateResolver));
6061
this.lockId = lockId;
6162
this.clock = clock;
6263
this.criterionOperatorRegistry = criterionOperatorRegistry;

core/common/lib/store-lib/src/main/java/org/eclipse/edc/store/ReflectionBasedQueryResolver.java

+10-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package org.eclipse.edc.store;
1616

17+
import org.eclipse.edc.spi.query.CriteriaToPredicate;
1718
import org.eclipse.edc.spi.query.CriterionOperatorRegistry;
1819
import org.eclipse.edc.spi.query.QueryResolver;
1920
import org.eclipse.edc.spi.query.QuerySpec;
@@ -22,8 +23,6 @@
2223
import org.jetbrains.annotations.NotNull;
2324

2425
import java.util.Comparator;
25-
import java.util.function.BinaryOperator;
26-
import java.util.function.Predicate;
2726
import java.util.stream.Stream;
2827

2928
import static java.lang.String.format;
@@ -36,7 +35,7 @@
3635
public class ReflectionBasedQueryResolver<T> implements QueryResolver<T> {
3736

3837
private final Class<T> typeParameterClass;
39-
private final CriterionOperatorRegistry criterionOperatorRegistry;
38+
private final CriteriaToPredicate<T> criteriaToPredicate;
4039

4140
/**
4241
* Constructor for ReflectionBasedQueryResolver
@@ -45,10 +44,13 @@ public class ReflectionBasedQueryResolver<T> implements QueryResolver<T> {
4544
* @param criterionOperatorRegistry converts from a criterion to a predicate
4645
*/
4746
public ReflectionBasedQueryResolver(Class<T> typeParameterClass, CriterionOperatorRegistry criterionOperatorRegistry) {
48-
this.typeParameterClass = typeParameterClass;
49-
this.criterionOperatorRegistry = criterionOperatorRegistry;
47+
this(typeParameterClass, new AndOperatorCriteriaToPredicate<>(criterionOperatorRegistry));
5048
}
5149

50+
public ReflectionBasedQueryResolver(Class<T> typeParameterClass, CriteriaToPredicate<T> criteriaToPredicate) {
51+
this.typeParameterClass = typeParameterClass;
52+
this.criteriaToPredicate = criteriaToPredicate;
53+
}
5254

5355
/**
5456
* Method to query a stream by provided specification.
@@ -58,16 +60,13 @@ public ReflectionBasedQueryResolver(Class<T> typeParameterClass, CriterionOperat
5860
*
5961
* @param stream stream to be queried.
6062
* @param spec query specification.
61-
* @param accumulator accumulation operation, e.g. Predicate::and, Predicate::or, etc.
6263
* @return stream result from queries.
6364
*/
6465
@Override
65-
public Stream<T> query(Stream<T> stream, QuerySpec spec, BinaryOperator<Predicate<Object>> accumulator, Predicate<Object> fallback) {
66-
var andPredicate = spec.getFilterExpression().stream()
67-
.map(criterionOperatorRegistry::toPredicate)
68-
.reduce(fallback, accumulator);
66+
public Stream<T> query(Stream<T> stream, QuerySpec spec) {
67+
var predicate = criteriaToPredicate.convert(spec.getFilterExpression());
6968

70-
var filteredStream = stream.filter(andPredicate);
69+
var filteredStream = stream.filter(predicate);
7170

7271
// sort
7372
var sortField = spec.getSortField();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.store;
16+
17+
import org.eclipse.edc.spi.entity.StateResolver;
18+
import org.eclipse.edc.spi.entity.StatefulEntity;
19+
import org.eclipse.edc.spi.query.CriteriaToPredicate;
20+
import org.eclipse.edc.spi.query.Criterion;
21+
import org.eclipse.edc.spi.query.CriterionOperatorRegistry;
22+
23+
import java.util.List;
24+
import java.util.function.Predicate;
25+
26+
import static org.eclipse.edc.spi.query.Criterion.criterion;
27+
28+
/**
29+
* Convert criteria to predicate for stateful entities.
30+
*
31+
* @param <E> entity type
32+
*/
33+
public class StatefulEntityCriteriaToPredicate<E extends StatefulEntity<E>> implements CriteriaToPredicate<E> {
34+
35+
private final CriterionOperatorRegistry criterionOperatorRegistry;
36+
private final StateResolver stateResolver;
37+
38+
public StatefulEntityCriteriaToPredicate(CriterionOperatorRegistry criterionOperatorRegistry, StateResolver stateResolver) {
39+
this.criterionOperatorRegistry = criterionOperatorRegistry;
40+
this.stateResolver = stateResolver;
41+
}
42+
43+
@Override
44+
public Predicate<E> convert(List<Criterion> criteria) {
45+
return criteria.stream()
46+
.map(criterion -> {
47+
if (criterion.getOperandLeft().equals("state") && criterion.getOperandRight() instanceof String stateString) {
48+
return criterion(criterion.getOperandLeft(), criterion.getOperator(), stateResolver.resolve(stateString));
49+
}
50+
return criterion;
51+
})
52+
.map(criterionOperatorRegistry::<E>toPredicate)
53+
.reduce(x -> true, Predicate::and);
54+
}
55+
}

core/common/lib/store-lib/src/test/java/org/eclipse/edc/store/ReflectionBasedQueryResolverTest.java

-11
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.util.Comparator;
2626
import java.util.List;
2727
import java.util.Objects;
28-
import java.util.function.Predicate;
2928
import java.util.stream.Collectors;
3029
import java.util.stream.IntStream;
3130
import java.util.stream.Stream;
@@ -57,16 +56,6 @@ void verifyQuery_equalStringProperty() {
5756

5857
}
5958

60-
@Test
61-
void verifyQuery_equalStringProperty_accumulateOr() {
62-
var stream = Stream.concat(
63-
IntStream.range(0, 5).mapToObj(i -> new FakeItem(i, "Alice")),
64-
IntStream.range(5, 10).mapToObj(i -> new FakeItem(i, "Bob")));
65-
66-
var spec = QuerySpec.Builder.newInstance().filter(List.of(criterion("name", "=", "Alice"), criterion("name", "=", "Bob"))).build();
67-
assertThat(queryResolver.query(stream, spec, Predicate::or, x -> false)).hasSize(10).extracting(FakeItem::getName).containsOnly("Bob", "Alice");
68-
}
69-
7059
@Test
7160
void verifyQuery_criterionFilterIntProperty() {
7261
var stream = Stream.concat(

0 commit comments

Comments
 (0)