Add aggregate functions and streams in queryable store

First, aggregate functions for minimum, maximum, sum and average
have been added to the queryable store API. These can be used
with the query fields, which have been enhanced for this.

Second, an additional stream like API has been added to retrieve
collections to avoid the creation of huge result objects.
This commit is contained in:
Rene Pfeuffer
2025-04-09 13:12:33 +02:00
parent 395a2edeb3
commit 01e8d493d2
18 changed files with 443 additions and 136 deletions

View File

@@ -35,7 +35,7 @@ import java.lang.annotation.Target;
* possible to store objects related to repositories; with this annotation it is possible to use other objects as
* parents, too, like for instance users).
*
* @since 3.7.0
* @since 3.8.0
*/
@Documented
@Target(ElementType.TYPE)

View File

@@ -20,21 +20,17 @@ import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
class NumberQueryFieldHandler extends QueryFieldHandler {
public NumberQueryFieldHandler(String packageName, String className) {
this(packageName, className, null);
}
public NumberQueryFieldHandler(String packageName, String className, String suffix) {
NumberQueryFieldHandler(String className) {
super(
"NumberQueryField",
new TypeName[]{ClassName.get(packageName, className)},
className + "QueryField",
new TypeName[]{},
(fieldBuilder, element, fieldClass, fieldName) -> fieldBuilder
.initializer(
"new $T<>($S)",
ClassName.get("sonia.scm.store", "QueryableStore").nestedClass(fieldClass),
fieldName
),
suffix
null
);
}
}

View File

@@ -264,19 +264,15 @@ class QueryFieldClassCreator {
SIMPLE_INITIALIZER));
case "int", "java.lang.Integer" -> List.of(
new NumberQueryFieldHandler(
"java.lang",
"Integer"));
case "long", "java.lang.Long" -> List.of(
new NumberQueryFieldHandler(
"java.lang",
"Long"));
case "float", "java.lang.Float" -> List.of(
new NumberQueryFieldHandler(
"java.lang",
"Float"));
case "double", "java.lang.Double" -> List.of(
new NumberQueryFieldHandler(
"java.lang",
"Double"));
case "java.util.Date", "java.time.Instant" -> List.of(
new QueryFieldHandler(

View File

@@ -16,35 +16,31 @@
package sonia.scm.testing;
import java.lang.Double;
import java.lang.Float;
import java.lang.Integer;
import java.lang.Long;
import sonia.scm.store.QueryableStore;
public final class DQueryFields {
public static final QueryableStore.IdQueryField<D> INTERNAL_ID =
new QueryableStore.IdQueryField<>();
public static final QueryableStore.NumberQueryField<D, Integer> AGE =
new QueryableStore.NumberQueryField<>("age");
public static final QueryableStore.NumberQueryField<D, Integer> WEIGHT =
new QueryableStore.NumberQueryField<>("weight");
public static final QueryableStore.IntegerQueryField<D> AGE =
new QueryableStore.IntegerQueryField<>("age");
public static final QueryableStore.IntegerQueryField<D> WEIGHT =
new QueryableStore.IntegerQueryField<>("weight");
public static final QueryableStore.NumberQueryField<D, Long> CREATIONTIME =
new QueryableStore.NumberQueryField<>("creationTime");
public static final QueryableStore.NumberQueryField<D, Long> LASTMODIFIED =
new QueryableStore.NumberQueryField<>("lastModified");
public static final QueryableStore.LongQueryField<D> CREATIONTIME =
new QueryableStore.LongQueryField<>("creationTime");
public static final QueryableStore.LongQueryField<D> LASTMODIFIED =
new QueryableStore.LongQueryField<>("lastModified");
public static final QueryableStore.NumberQueryField<D, Float> HEIGHT =
new QueryableStore.NumberQueryField<>("height");
public static final QueryableStore.NumberQueryField<D, Float> WIDTH =
new QueryableStore.NumberQueryField<>("width");
public static final QueryableStore.FloatQueryField<D> HEIGHT =
new QueryableStore.FloatQueryField<>("height");
public static final QueryableStore.FloatQueryField<D> WIDTH =
new QueryableStore.FloatQueryField<>("width");
public static final QueryableStore.NumberQueryField<D, Double> PRICE =
new QueryableStore.NumberQueryField<>("price");
public static final QueryableStore.NumberQueryField<D, Double> MARGIN =
new QueryableStore.NumberQueryField<>("margin");
public static final QueryableStore.DoubleQueryField<D> PRICE =
new QueryableStore.DoubleQueryField<>("price");
public static final QueryableStore.DoubleQueryField<D> MARGIN =
new QueryableStore.DoubleQueryField<>("margin");
private DQueryFields() {
}

View File

@@ -80,7 +80,7 @@ public interface ExtensionProcessor
/**
* Returns all queryable types.
* @since 3.7.0
* @since 3.8.0
*/
default Iterable<QueryableTypeDescriptor> getQueryableTypes() {
return emptySet();

View File

@@ -116,7 +116,7 @@ public class ScmModule {
}
/**
* @since 3.7.0
* @since 3.8.0
*/
public Iterable<QueryableTypeDescriptor> getQueryableTypes() {
return nonNull(queryableTypes);

View File

@@ -29,7 +29,7 @@ import java.util.function.BooleanSupplier;
* processor for the annotated type.
*
* @param <T> The type of the objects to query.
* @since 3.7.0
* @since 3.8.0
*/
public interface QueryableMutableStore<T> extends DataStore<T>, QueryableStore<T>, AutoCloseable {
void transactional(BooleanSupplier callback);

View File

@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
/**
* This interface is used to query objects annotated with {@link QueryableType}. It will be created by the
@@ -32,7 +33,7 @@ import java.util.Optional;
* processor for the annotated type.
*
* @param <T> The type of the objects to query.
* @since 3.7.0
* @since 3.8.0
*/
public interface QueryableStore<T> extends AutoCloseable {
@@ -90,7 +91,9 @@ public interface QueryableStore<T> extends AutoCloseable {
/**
* Returns all objects that match the query. If the query returns no result, an empty list will be returned.
*/
List<T_RESULT> findAll();
default List<T_RESULT> findAll() {
return findAll(0, Integer.MAX_VALUE);
}
/**
* Returns a subset of all objects that match the query. If the query returns no result or the {@code offset} and
@@ -101,6 +104,24 @@ public interface QueryableStore<T> extends AutoCloseable {
*/
List<T_RESULT> findAll(long offset, long limit);
/**
* Calls the given consumer for all objects that match the query.
*
* @param consumer The consumer that will be called for each single found object.
*/
default void forEach(Consumer<T_RESULT> consumer) {
forEach(consumer, 0, Integer.MAX_VALUE);
}
/**
* Calls the given consumer for a subset of all objects that match the query.
*
* @param consumer The consumer that will be called for each single found object.
* @param offset The offset to start feeding results to the consumer.
* @param limit The maximum number of results.
*/
void forEach(Consumer<T_RESULT> consumer, long offset, long limit);
/**
* Returns the found objects in combination with the parent ids they belong to. This is useful if you are using a
* queryable store that is not scoped to specific parent objects, and you therefore want to know to which parent
@@ -124,6 +145,26 @@ public interface QueryableStore<T> extends AutoCloseable {
* Returns the count of all objects that match the query.
*/
long count();
/**
* Returns the minimum value of the given field that match the query.
*/
<A> A min(AggregatableQueryField<T, A> field);
/**
* Returns the maximum value of the given field that match the query.
*/
<A> A max(AggregatableQueryField<T, A> field);
/**
* Returns the sum of the given field that match the query.
*/
<A> A sum(AggregatableNumberQueryField<T, A> field);
/**
* Returns the average value of the given field that match the query.
*/
<A> Double average(AggregatableNumberQueryField<T, A> field);
}
/**
@@ -160,10 +201,17 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param <F> The type of the field.
*/
@SuppressWarnings("unused")
class QueryField<T, F> {
final String name;
interface QueryField<T, F> {
String getName();
public QueryField(String name) {
boolean isIdField();
}
@SuppressWarnings("unused")
abstract class BaseQueryField<T, F> implements QueryField<T, F> {
private final String name;
BaseQueryField(String name) {
this.name = name;
}
@@ -185,6 +233,19 @@ public interface QueryableStore<T> extends AutoCloseable {
}
}
/**
* Query fields implementing this can compute aggregates like minimum or maximum.
*/
interface AggregatableQueryField<T, A> extends QueryField<T, A> {
Class<A> getFieldType();
}
/**
* Query fields implementing this can compute aggregates like sum or average.
*/
interface AggregatableNumberQueryField<T, A> extends AggregatableQueryField<T, A> {
}
/**
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
* processor for each {@link String} field of a class annotated with {@link QueryableType}.
@@ -193,7 +254,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class StringQueryField<T> extends QueryField<T, String> {
class StringQueryField<T> extends BaseQueryField<T, String> implements AggregatableQueryField<T, String> {
public StringQueryField(String name) {
super(name);
@@ -239,6 +300,11 @@ public interface QueryableStore<T> extends AutoCloseable {
public Condition<T> in(Collection<String> values) {
return in(values.toArray(new String[0]));
}
@Override
public Class<String> getFieldType() {
return String.class;
}
}
/**
@@ -274,9 +340,9 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param <T> The type of the objects this condition is used for.
* @param <N> The type of the number field.
*/
class NumberQueryField<T, N extends Number> extends QueryField<T, N> {
abstract class NumberQueryField<T, N extends Number> extends BaseQueryField<T, N> {
public NumberQueryField(String name) {
NumberQueryField(String name) {
super(name);
}
@@ -362,6 +428,50 @@ public interface QueryableStore<T> extends AutoCloseable {
}
}
class IntegerQueryField<T> extends NumberQueryField<T, Integer> implements AggregatableNumberQueryField<T, Integer> {
public IntegerQueryField(String name) {
super(name);
}
@Override
public Class<Integer> getFieldType() {
return Integer.class;
}
}
class LongQueryField<T> extends NumberQueryField<T, Long> implements AggregatableNumberQueryField<T, Long> {
public LongQueryField(String name) {
super(name);
}
@Override
public Class<Long> getFieldType() {
return Long.class;
}
}
class FloatQueryField<T> extends NumberQueryField<T, Float> implements AggregatableNumberQueryField<T, Float> {
public FloatQueryField(String name) {
super(name);
}
@Override
public Class<Float> getFieldType() {
return Float.class;
}
}
class DoubleQueryField<T> extends NumberQueryField<T, Double> implements AggregatableNumberQueryField<T, Double> {
public DoubleQueryField(String name) {
super(name);
}
@Override
public Class<Double> getFieldType() {
return Double.class;
}
}
/**
* This class is used to create conditions for queries. Instances of this class will be created by the annotation
* processor for each date field of a class annotated with {@link QueryableType}.
@@ -370,7 +480,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class InstantQueryField<T> extends QueryField<T, Instant> {
class InstantQueryField<T> extends BaseQueryField<T, Instant> {
public InstantQueryField(String name) {
super(name);
}
@@ -467,7 +577,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class BooleanQueryField<T> extends QueryField<T, Boolean> {
class BooleanQueryField<T> extends BaseQueryField<T, Boolean> {
public BooleanQueryField(String name) {
super(name);
@@ -511,7 +621,7 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param <T> The type of the objects this condition is used for.
* @param <E> The type of the enum field.
*/
class EnumQueryField<T, E extends Enum<E>> extends QueryField<T, Enum<E>> {
class EnumQueryField<T, E extends Enum<E>> extends BaseQueryField<T, Enum<E>> {
public EnumQueryField(String name) {
super(name);
}
@@ -556,7 +666,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class CollectionQueryField<T> extends QueryField<T, Object> {
class CollectionQueryField<T> extends BaseQueryField<T, Object> {
public CollectionQueryField(String name) {
super(name);
}
@@ -582,7 +692,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class CollectionSizeQueryField<T> extends NumberQueryField<T, Long> {
class CollectionSizeQueryField<T> extends NumberQueryField<T, Long> implements AggregatableNumberQueryField<T, Long> {
public CollectionSizeQueryField(String name) {
super(name);
}
@@ -595,6 +705,11 @@ public interface QueryableStore<T> extends AutoCloseable {
public Condition<T> isEmpty() {
return eq(0L);
}
@Override
public Class<Long> getFieldType() {
return Long.class;
}
}
/**
@@ -606,7 +721,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class MapQueryField<T> extends QueryField<T, Object> {
class MapQueryField<T> extends BaseQueryField<T, Object> {
public MapQueryField(String name) {
super(name);
}
@@ -642,7 +757,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @param <T> The type of the objects this condition is used for.
*/
class MapSizeQueryField<T> extends NumberQueryField<T, Long> {
class MapSizeQueryField<T> extends NumberQueryField<T, Long> implements AggregatableNumberQueryField<T, Long> {
public MapSizeQueryField(String name) {
super(name);
}
@@ -655,6 +770,11 @@ public interface QueryableStore<T> extends AutoCloseable {
public Condition<T> isEmpty() {
return eq(0L);
}
@Override
public Class<Long> getFieldType() {
return Long.class;
}
}
/**

View File

@@ -29,7 +29,7 @@ package sonia.scm.store;
* Implementations probably are backed by a database or a similar storage system instead of the familiar
* file based storage using XML.
*
* @since 3.7.0
* @since 3.8.0
*/
public interface QueryableStoreFactory {

View File

@@ -21,7 +21,7 @@ import sonia.scm.store.StoreException;
/**
* This exception is thrown if a name for a store element doesn't meet the internal verification requirements.
*
* @since 3.7.0
* @since 3.8.0
*/
class BadStoreNameException extends StoreException {
BadStoreNameException(String badName) {

View File

@@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import sonia.scm.store.QueryableStore;
class SQLAggregate extends SQLField {
public SQLAggregate(String operator, QueryableStore.AggregatableQueryField<?, ?> queryField) {
super(operator + "(" + SQLFieldHelper.computeSQLField(queryField) + ") ");
}
}

View File

@@ -26,12 +26,10 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.Instant;
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier;
/**
* <b>SQLCondition</b> represents a condition given in an agnostic SQL statement.
*
* @since 3.7.0
* @since 3.8.0
*/
@Getter
@Setter
@@ -96,14 +94,8 @@ class SQLCondition implements SQLNodeWithValue {
return "select * from json_each(payload, '$." + queryField.getName() + "') where ";
} else if (queryField instanceof QueryableStore.InstantQueryField) {
return "json_extract(payload, '$." + queryField.getName() + "')";
} else if (queryField instanceof QueryableStore.CollectionSizeQueryField<?>) {
return "json_array_length(payload, '$." + queryField.getName() + "')";
} else if (queryField instanceof QueryableStore.MapSizeQueryField<?>) {
return "(select count(*) from json_each(payload, '$." + queryField.getName() + "')) ";
} else if (queryField.isIdField()) {
return computeColumnIdentifier(queryField.getName());
} else {
return "json_extract(payload, '$." + queryField.getName() + "')";
return SQLFieldHelper.computeSQLField(queryField);
}
}

View File

@@ -21,7 +21,7 @@ import lombok.Getter;
/**
* Representation of a value of a row within an {@link SQLTable}.
*
* @since 3.7.0
* @since 3.8.0
*/
@Getter
class SQLField implements SQLNode {

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import sonia.scm.store.QueryableStore;
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier;
final class SQLFieldHelper {
private SQLFieldHelper() {
}
static String computeSQLField(QueryableStore.QueryField<?, ?> queryField) {
if (queryField instanceof QueryableStore.CollectionSizeQueryField<?>) {
return "json_array_length(payload, '$." + queryField.getName() + "')";
} else if (queryField instanceof QueryableStore.MapSizeQueryField<?>) {
return "(select count(*) from json_each(payload, '$." + queryField.getName() + "')) ";
} else if (queryField.isIdField()) {
return computeColumnIdentifier(queryField.getName());
} else {
return "json_extract(payload, '$." + queryField.getName() + "')";
}
}
}

View File

@@ -25,7 +25,7 @@ import java.util.List;
/**
* Representation of a column or a list of columns within an {@link SQLTable}.
*
* @since 3.7.0
* @since 3.8.0
*/
@Slf4j
class SQLValue implements SQLNodeWithValue {

View File

@@ -48,6 +48,7 @@ import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier;
@@ -305,12 +306,14 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
}
@Override
public List<T_RESULT> findAll() {
return findAll(0, Integer.MAX_VALUE);
public List<T_RESULT> findAll(long offset, long limit) {
List<T_RESULT> result = new ArrayList<>();
forEach(result::add, offset, limit);
return Collections.unmodifiableList(result);
}
@Override
public List<T_RESULT> findAll(long offset, long limit) {
public void forEach(Consumer<T_RESULT> consumer, long offset, long limit) {
StringBuilder orderByBuilder = new StringBuilder();
if (orderBy != null && !orderBy.isEmpty()) {
toOrderBySQL(orderByBuilder);
@@ -326,15 +329,15 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
offset
);
return executeRead(
executeRead(
sqlSelectQuery,
statement -> {
List<T_RESULT> result = new ArrayList<>();
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
result.add(extractResult(resultSet));
try (ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
consumer.accept(extractResult(resultSet));
}
}
return Collections.unmodifiableList(result);
return null;
}
);
}
@@ -366,6 +369,49 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
);
}
@Override
public <A> A min(AggregatableQueryField<T, A> field) {
return aggregate(field, "MIN", field.getFieldType());
}
@Override
public <A> A max(AggregatableQueryField<T, A> field) {
return aggregate(field, "MAX", field.getFieldType());
}
@Override
public <A> A sum(AggregatableNumberQueryField<T, A> field) {
return aggregate(field, "SUM", field.getFieldType());
}
@Override
public <A> Double average(AggregatableNumberQueryField<T, A> field) {
return aggregate(field, "AVG", Double.class);
}
private <A> A aggregate(AggregatableQueryField<T, ?> field, String aggregate, Class<A> resultType) {
SQLSelectStatement sqlStatementQuery =
new SQLSelectStatement(
List.of(new SQLAggregate(aggregate, field)),
computeFromTable(),
computeCondition()
);
return executeRead(
sqlStatementQuery,
statement -> {
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
if (resultSet.getObject(1) == null) {
return null;
}
return resultSet.getObject(1, resultType);
}
throw new IllegalStateException("failed to read count for type " + queryableTypeDescriptor);
}
);
}
@Override
public Query<T, T_RESULT> orderBy(QueryField<T, ?> field, Order order) {
List<OrderBy<T>> extendedOrderBy = new ArrayList<>(this.orderBy);

View File

@@ -64,7 +64,7 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("Heart Of Gold", Range.INTER_GALACTIC));
List<Spaceship> all = store
.query(SPACESHIP_RANGE_ENUM_QUERY_FIELD.eq(Range.SOLAR_SYSTEM))
.query(SPACESHIP_RANGE.eq(Range.SOLAR_SYSTEM))
.findAll();
assertThat(all).hasSize(1);
@@ -82,7 +82,7 @@ class SQLiteQueryableStoreTest {
store.put(arthur);
List<User> all = store.query(
CREATION_DATE_QUERY_FIELD.lessOrEquals(9999999999L)
CREATION_DATE.lessOrEquals(9999999999L)
)
.findAll();
@@ -102,7 +102,7 @@ class SQLiteQueryableStoreTest {
store.put(arthur);
List<User> all = store.query(
CREATION_DATE_AS_INTEGER_QUERY_FIELD.less(40)
CREATION_DATE_AS_INTEGER.less(40)
)
.findAll();
@@ -122,7 +122,7 @@ class SQLiteQueryableStoreTest {
store.put(arthur);
List<User> all = store.query(
ACTIVE_QUERY_FIELD.isTrue()
ACTIVE.isTrue()
)
.findAll();
@@ -142,7 +142,7 @@ class SQLiteQueryableStoreTest {
store.put(arthur);
long count = store.query(
ACTIVE_QUERY_FIELD.isTrue()
ACTIVE.isTrue()
)
.count();
@@ -161,7 +161,7 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
List<Spaceship> result = store.query(
SPACESHIP_CREW_QUERY_FIELD.contains("Marvin")
SPACESHIP_CREW.contains("Marvin")
).findAll();
assertThat(result).hasSize(1);
@@ -174,7 +174,7 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
long result = store.query(
SPACESHIP_CREW_QUERY_FIELD.contains("Marvin")
SPACESHIP_CREW.contains("Marvin")
).count();
assertThat(result).isEqualTo(1);
@@ -191,6 +191,90 @@ class SQLiteQueryableStoreTest {
assertThat(result).isEqualTo(2);
}
@Test
void shouldHandleEmptyCollectionWithMaxString() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Integer result = store.query().max(
SPACESHIP_FLIGHT_COUNT
);
assertThat(result).isNull();
}
@Nested
class ForAggregations {
SQLiteQueryableMutableStore<Spaceship> store;
@BeforeEach
void createData() {
store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship spaceshuttle = new Spaceship("Spaceshuttle", "Buzz", "Anndre");
spaceshuttle.setFlightCount(12);
store.put("Spaceshuttle", spaceshuttle);
Spaceship heartOfGold = new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin");
heartOfGold.setFlightCount(42);
store.put("Heart Of Gold", heartOfGold);
Spaceship vogon = new Spaceship("Vogon", "Prostetnic Vogon Jeltz");
vogon.setFlightCount(321);
store.put("Vogon", vogon);
}
@Test
void shouldGetMaxString() {
String result = store.query().max(
SPACESHIP_NAME
);
assertThat(result).isEqualTo("Vogon");
}
@Test
void shouldGetMaxOfCollectionSize() {
Long result = store.query().max(
SPACESHIP_CREW_SIZE
);
assertThat(result).isEqualTo(5);
}
@Test
void shouldGetMinOfId() {
String result = store.query().min(
SPACESHIP_ID
);
assertThat(result).isEqualTo("Heart Of Gold");
}
@Test
void shouldGetMinNumber() {
int result = store.query().min(
SPACESHIP_FLIGHT_COUNT
);
assertThat(result).isEqualTo(12);
}
@Test
void shouldGetAverageNumber() {
double result = store.query().average(
SPACESHIP_FLIGHT_COUNT
);
assertThat(result).isEqualTo(125);
}
@Test
void shouldGetSum() {
int result = store.query().sum(
SPACESHIP_FLIGHT_COUNT
);
assertThat(result).isEqualTo(375);
}
}
@Test
void shouldHandleCollectionSize() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
@@ -199,20 +283,20 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("MillenniumFalcon"));
List<Spaceship> onlyEmpty = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.isEmpty()
SPACESHIP_CREW_SIZE.isEmpty()
).findAll();
assertThat(onlyEmpty).hasSize(1);
assertThat(onlyEmpty.get(0).getName()).isEqualTo("MillenniumFalcon");
List<Spaceship> exactlyTwoCrewMates = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.eq(2L)
SPACESHIP_CREW_SIZE.eq(2L)
).findAll();
assertThat(exactlyTwoCrewMates).hasSize(1);
assertThat(exactlyTwoCrewMates.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> moreThanTwoCrewMates = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.greater(2L)
SPACESHIP_CREW_SIZE.greater(2L)
).findAll();
assertThat(moreThanTwoCrewMates).hasSize(1);
assertThat(moreThanTwoCrewMates.get(0).getName()).isEqualTo("Heart of Gold");
@@ -226,13 +310,13 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("MillenniumFalcon", Map.of("dagobah", false)));
List<Spaceship> keyResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsKey("vogon")
SPACESHIP_DESTINATIONS.containsKey("vogon")
).findAll();
assertThat(keyResult).hasSize(1);
assertThat(keyResult.get(0).getName()).isEqualTo("Heart of Gold");
List<Spaceship> valueResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsValue(false)
SPACESHIP_DESTINATIONS.containsValue(false)
).findAll();
assertThat(valueResult).hasSize(1);
assertThat(valueResult.get(0).getName()).isEqualTo("MillenniumFalcon");
@@ -246,14 +330,14 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("MillenniumFalcon", Map.of("dagobah", false)));
long keyResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsKey("vogon")
SPACESHIP_DESTINATIONS.containsKey("vogon")
).count();
assertThat(keyResult).isEqualTo(1);
long valueResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsValue(false)
SPACESHIP_DESTINATIONS.containsValue(false)
).count();
assertThat(valueResult).isEqualTo(1);
}
@@ -267,20 +351,20 @@ class SQLiteQueryableStoreTest {
store.put(new Spaceship("MillenniumFalcon", Map.of()));
List<Spaceship> onlyEmpty = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.isEmpty()
SPACESHIP_DESTINATIONS_SIZE.isEmpty()
).findAll();
assertThat(onlyEmpty).hasSize(1);
assertThat(onlyEmpty.get(0).getName()).isEqualTo("MillenniumFalcon");
List<Spaceship> exactlyTwoDestinations = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.eq(2L)
SPACESHIP_DESTINATIONS_SIZE.eq(2L)
).findAll();
assertThat(exactlyTwoDestinations).hasSize(1);
assertThat(exactlyTwoDestinations.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> moreThanTwoDestinations = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.greater(2L)
SPACESHIP_DESTINATIONS_SIZE.greater(2L)
).findAll();
assertThat(moreThanTwoDestinations).hasSize(1);
assertThat(moreThanTwoDestinations.get(0).getName()).isEqualTo("Heart of Gold");
@@ -298,22 +382,22 @@ class SQLiteQueryableStoreTest {
store.put(falcon);
List<Spaceship> resultEqOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.eq(Instant.parse("2015-12-21T10:00:00Z"))).findAll();
SPACESHIP_INSERVICE.eq(Instant.parse("2015-12-21T10:00:00Z"))).findAll();
assertThat(resultEqOperator).hasSize(1);
assertThat(resultEqOperator.get(0).getName()).isEqualTo("Falcon9");
List<Spaceship> resultBeforeOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.before(Instant.parse("2000-12-21T10:00:00Z"))).findAll();
SPACESHIP_INSERVICE.before(Instant.parse("2000-12-21T10:00:00Z"))).findAll();
assertThat(resultBeforeOperator).hasSize(1);
assertThat(resultBeforeOperator.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> resultAfterOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.after(Instant.parse("2000-01-01T00:00:00Z"))).findAll();
SPACESHIP_INSERVICE.after(Instant.parse("2000-01-01T00:00:00Z"))).findAll();
assertThat(resultAfterOperator).hasSize(1);
assertThat(resultAfterOperator.get(0).getName()).isEqualTo("Falcon9");
List<Spaceship> resultBetweenOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.between(Instant.parse("1980-04-12T10:00:00Z"), Instant.parse("2016-12-21T10:00:00Z"))).findAll();
SPACESHIP_INSERVICE.between(Instant.parse("1980-04-12T10:00:00Z"), Instant.parse("2016-12-21T10:00:00Z"))).findAll();
assertThat(resultBetweenOperator).hasSize(2);
}
@@ -344,8 +428,8 @@ class SQLiteQueryableStoreTest {
store.put(new User("marvin", "Marvin", "marvin@hog.org"));
List<User> all = store.query()
.orderBy(USER_NAME_QUERY_FIELD, QueryableStore.Order.ASC)
.orderBy(DISPLAY_NAME_QUERY_FIELD, QueryableStore.Order.DESC)
.orderBy(USER_NAME, QueryableStore.Order.ASC)
.orderBy(DISPLAY_NAME, QueryableStore.Order.DESC)
.findAll();
assertThat(all)
@@ -364,7 +448,7 @@ class SQLiteQueryableStoreTest {
store.put("3", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
ID_QUERY_FIELD.eq("1")
ID.eq("1")
)
.findAll();
@@ -385,7 +469,7 @@ class SQLiteQueryableStoreTest {
SQLiteQueryableStore<User> store = new StoreTestBuilder(connectionString, Group.class.getName()).withIds();
List<User> all = store.query(
GROUP_QUERY_FIELD.eq("42")
GROUP.eq("42")
)
.findAll();
@@ -402,7 +486,7 @@ class SQLiteQueryableStoreTest {
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.contains("ri")
USER_NAME.contains("ri")
)
.findAll();
@@ -416,7 +500,7 @@ class SQLiteQueryableStoreTest {
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
DISPLAY_NAME_QUERY_FIELD.isNull()
DISPLAY_NAME.isNull()
)
.findAll();
@@ -432,7 +516,7 @@ class SQLiteQueryableStoreTest {
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
Conditions.not(DISPLAY_NAME_QUERY_FIELD.isNull())
Conditions.not(DISPLAY_NAME.isNull())
)
.findAll();
@@ -450,8 +534,8 @@ class SQLiteQueryableStoreTest {
List<User> all = store.query(
Conditions.or(
DISPLAY_NAME_QUERY_FIELD.eq("Tricia"),
DISPLAY_NAME_QUERY_FIELD.eq("Trillian McMillan")
DISPLAY_NAME.eq("Tricia"),
DISPLAY_NAME.eq("Trillian McMillan")
)
)
.findAll();
@@ -510,7 +594,7 @@ class SQLiteQueryableStoreTest {
.withIds("1337")
.put("tricia", new User("trillian", "Trillian McMillan", "tricia@hog.org"));
List<User> all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll();
List<User> all = store.query(USER_NAME.eq("trillian")).findAll();
assertThat(all)
.extracting("displayName")
@@ -525,7 +609,7 @@ class SQLiteQueryableStoreTest {
store.put(new User("zaphod", "Beeblebrox", "zaphod@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.in("trillian", "arthur")
USER_NAME.in("trillian", "arthur")
)
.findAll();
@@ -551,7 +635,7 @@ class SQLiteQueryableStoreTest {
store.put("tricia", new User("trillian"));
store.put("dent", new User("arthur"));
List<User> all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll();
List<User> all = store.query(USER_NAME.eq("trillian")).findAll();
assertThat(all).hasSize(1);
}
@@ -582,8 +666,8 @@ class SQLiteQueryableStoreTest {
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.eq("trillian"),
DISPLAY_NAME_QUERY_FIELD.eq("Tricia")
USER_NAME.eq("trillian"),
DISPLAY_NAME.eq("Tricia")
)
.findAll();
@@ -614,7 +698,7 @@ class SQLiteQueryableStoreTest {
@Test
void shouldReturnEmptyOptionalIfNoResultFound() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
assertThat(store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne()).isEmpty();
assertThat(store.query(SPACESHIP_NAME.eq("Heart Of Gold")).findOne()).isEmpty();
}
@Test
@@ -622,7 +706,7 @@ class SQLiteQueryableStoreTest {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship expectedShip = new Spaceship("Heart Of Gold", Range.INNER_GALACTIC);
store.put(expectedShip);
Spaceship ship = store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne().get();
Spaceship ship = store.query(SPACESHIP_NAME.eq("Heart Of Gold")).findOne().get();
assertThat(ship).isEqualTo(expectedShip);
}
@@ -634,7 +718,7 @@ class SQLiteQueryableStoreTest {
Spaceship localShip = new Spaceship("Heart Of Gold", Range.SOLAR_SYSTEM);
store.put(expectedShip);
store.put(localShip);
assertThatThrownBy(() -> store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne().get())
assertThatThrownBy(() -> store.query(SPACESHIP_NAME.eq("Heart Of Gold")).findOne().get())
.isInstanceOf(QueryableStore.TooManyResultsException.class);
}
}
@@ -651,7 +735,7 @@ class SQLiteQueryableStoreTest {
store.put("3", new User("arthur", "Arthur Dent", "arthur@hog.org"));
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("trillian")
USER_NAME.eq("trillian")
)
.findFirst();
@@ -669,8 +753,8 @@ class SQLiteQueryableStoreTest {
store.put("4", new User("arthur", "Arthur Dent", "arthur@hog.org"));
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("trillian"),
MAIL_QUERY_FIELD.eq("mcmillan-alternate@gmail.com")
USER_NAME.eq("trillian"),
MAIL.eq("mcmillan-alternate@gmail.com")
)
.findFirst();
@@ -692,11 +776,11 @@ class SQLiteQueryableStoreTest {
Optional<User> user = store.query(
Conditions.and(
Conditions.and(
DISPLAY_NAME_QUERY_FIELD.eq("Trillian McMillan"),
MAIL_QUERY_FIELD.eq("mcmillan@gmail.com")
DISPLAY_NAME.eq("Trillian McMillan"),
MAIL.eq("mcmillan@gmail.com")
),
Conditions.not(
ID_QUERY_FIELD.eq("1")
ID.eq("1")
)
)
).findFirst();
@@ -708,7 +792,7 @@ class SQLiteQueryableStoreTest {
void shouldReturnEmptyOptionalIfNoResultFound() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("dave")
USER_NAME.eq("dave")
)
.findFirst();
assertThat(user).isEmpty();
@@ -870,39 +954,43 @@ class SQLiteQueryableStoreTest {
}
}
private static final QueryableStore.IdQueryField<User> ID_QUERY_FIELD =
private static final QueryableStore.IdQueryField<User> ID =
new QueryableStore.IdQueryField<>();
private static final QueryableStore.IdQueryField<User> GROUP_QUERY_FIELD =
private static final QueryableStore.IdQueryField<User> GROUP =
new QueryableStore.IdQueryField<>(Group.class);
private static final QueryableStore.StringQueryField<User> USER_NAME_QUERY_FIELD =
private static final QueryableStore.StringQueryField<User> USER_NAME =
new QueryableStore.StringQueryField<>("name");
private static final QueryableStore.StringQueryField<User> DISPLAY_NAME_QUERY_FIELD =
private static final QueryableStore.StringQueryField<User> DISPLAY_NAME =
new QueryableStore.StringQueryField<>("displayName");
private static final QueryableStore.StringQueryField<User> MAIL_QUERY_FIELD =
private static final QueryableStore.StringQueryField<User> MAIL =
new QueryableStore.StringQueryField<>("mail");
private static final QueryableStore.NumberQueryField<User, Long> CREATION_DATE_QUERY_FIELD =
new QueryableStore.NumberQueryField<>("creationDate");
private static final QueryableStore.NumberQueryField<User, Integer> CREATION_DATE_AS_INTEGER_QUERY_FIELD =
new QueryableStore.NumberQueryField<>("creationDate");
private static final QueryableStore.BooleanQueryField<User> ACTIVE_QUERY_FIELD =
private static final QueryableStore.LongQueryField<User> CREATION_DATE =
new QueryableStore.LongQueryField<>("creationDate");
private static final QueryableStore.IntegerQueryField<User> CREATION_DATE_AS_INTEGER =
new QueryableStore.IntegerQueryField<>("creationDate");
private static final QueryableStore.BooleanQueryField<User> ACTIVE =
new QueryableStore.BooleanQueryField<>("active");
enum Range {
SOLAR_SYSTEM, INNER_GALACTIC, INTER_GALACTIC
}
private static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_NAME_QUERY_FIELD =
private static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_ID =
new QueryableStore.IdQueryField<>();
private static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_NAME =
new QueryableStore.StringQueryField<>("name");
private static final QueryableStore.EnumQueryField<Spaceship, Range> SPACESHIP_RANGE_ENUM_QUERY_FIELD =
private static final QueryableStore.EnumQueryField<Spaceship, Range> SPACESHIP_RANGE =
new QueryableStore.EnumQueryField<>("range");
private static final QueryableStore.CollectionQueryField<Spaceship> SPACESHIP_CREW_QUERY_FIELD =
private static final QueryableStore.CollectionQueryField<Spaceship> SPACESHIP_CREW =
new QueryableStore.CollectionQueryField<>("crew");
private static final QueryableStore.CollectionSizeQueryField<Spaceship> SPACESHIP_CREW_SIZE_QUERY_FIELD =
private static final QueryableStore.CollectionSizeQueryField<Spaceship> SPACESHIP_CREW_SIZE =
new QueryableStore.CollectionSizeQueryField<>("crew");
private static final QueryableStore.MapQueryField<Spaceship> SPACESHIP_DESTINATIONS_QUERY_FIELD =
private static final QueryableStore.MapQueryField<Spaceship> SPACESHIP_DESTINATIONS =
new QueryableStore.MapQueryField<>("destinations");
private static final QueryableStore.MapSizeQueryField<Spaceship> SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD =
private static final QueryableStore.MapSizeQueryField<Spaceship> SPACESHIP_DESTINATIONS_SIZE =
new QueryableStore.MapSizeQueryField<>("destinations");
private static final QueryableStore.InstantQueryField<Spaceship> SPACESHIP_INSERVICE_QUERY_FIELD =
private static final QueryableStore.InstantQueryField<Spaceship> SPACESHIP_INSERVICE =
new QueryableStore.InstantQueryField<>("inServiceSince");
private static final QueryableStore.IntegerQueryField<Spaceship> SPACESHIP_FLIGHT_COUNT =
new QueryableStore.IntegerQueryField<>("flightCount");
}

View File

@@ -37,6 +37,7 @@ class Spaceship {
Collection<String> crew;
Map<String, Boolean> destinations;
Instant inServiceSince;
int flightCount;
public Spaceship() {
}
@@ -95,4 +96,12 @@ class Spaceship {
public void setInServiceSince(Instant inServiceSince) {
this.inServiceSince = inServiceSince;
}
public int getFlightCount() {
return flightCount;
}
public void setFlightCount(int flightCount) {
this.flightCount = flightCount;
}
}