From 01e8d493d2d2527bdfcb99673ad9ba68eef3de9c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 9 Apr 2025 13:12:33 +0200 Subject: [PATCH] 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. --- .../java/sonia/scm/store/QueryableType.java | 2 +- .../annotation/NumberQueryFieldHandler.java | 12 +- .../annotation/QueryFieldClassCreator.java | 4 - .../sonia/scm/testing/DQueryFields.java | 36 ++- .../sonia/scm/plugin/ExtensionProcessor.java | 2 +- .../main/java/sonia/scm/plugin/ScmModule.java | 2 +- .../scm/store/QueryableMutableStore.java | 2 +- .../java/sonia/scm/store/QueryableStore.java | 150 +++++++++++-- .../scm/store/QueryableStoreFactory.java | 2 +- .../store/sqlite/BadStoreNameException.java | 2 +- .../sonia/scm/store/sqlite/SQLAggregate.java | 25 +++ .../sonia/scm/store/sqlite/SQLCondition.java | 12 +- .../java/sonia/scm/store/sqlite/SQLField.java | 2 +- .../scm/store/sqlite/SQLFieldHelper.java | 39 ++++ .../java/sonia/scm/store/sqlite/SQLValue.java | 2 +- .../store/sqlite/SQLiteQueryableStore.java | 64 +++++- .../sqlite/SQLiteQueryableStoreTest.java | 212 +++++++++++++----- .../sonia/scm/store/sqlite/Spaceship.java | 9 + 18 files changed, 443 insertions(+), 136 deletions(-) create mode 100644 scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLAggregate.java create mode 100644 scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLFieldHelper.java diff --git a/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java b/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java index 10248530b5..baff25e490 100644 --- a/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java +++ b/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java @@ -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) diff --git a/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/NumberQueryFieldHandler.java b/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/NumberQueryFieldHandler.java index a6d5d11acc..66ab390db6 100644 --- a/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/NumberQueryFieldHandler.java +++ b/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/NumberQueryFieldHandler.java @@ -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 ); } } diff --git a/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/QueryFieldClassCreator.java b/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/QueryFieldClassCreator.java index 53cdf88caf..d342edba74 100644 --- a/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/QueryFieldClassCreator.java +++ b/scm-core-annotation-processor/src/main/java/sonia/scm/annotation/QueryFieldClassCreator.java @@ -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( diff --git a/scm-core-annotation-processor/src/test/resources/sonia/scm/testing/DQueryFields.java b/scm-core-annotation-processor/src/test/resources/sonia/scm/testing/DQueryFields.java index 808476c484..9e3c282894 100644 --- a/scm-core-annotation-processor/src/test/resources/sonia/scm/testing/DQueryFields.java +++ b/scm-core-annotation-processor/src/test/resources/sonia/scm/testing/DQueryFields.java @@ -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 INTERNAL_ID = new QueryableStore.IdQueryField<>(); - public static final QueryableStore.NumberQueryField AGE = - new QueryableStore.NumberQueryField<>("age"); - public static final QueryableStore.NumberQueryField WEIGHT = - new QueryableStore.NumberQueryField<>("weight"); + public static final QueryableStore.IntegerQueryField AGE = + new QueryableStore.IntegerQueryField<>("age"); + public static final QueryableStore.IntegerQueryField WEIGHT = + new QueryableStore.IntegerQueryField<>("weight"); - public static final QueryableStore.NumberQueryField CREATIONTIME = - new QueryableStore.NumberQueryField<>("creationTime"); - public static final QueryableStore.NumberQueryField LASTMODIFIED = - new QueryableStore.NumberQueryField<>("lastModified"); + public static final QueryableStore.LongQueryField CREATIONTIME = + new QueryableStore.LongQueryField<>("creationTime"); + public static final QueryableStore.LongQueryField LASTMODIFIED = + new QueryableStore.LongQueryField<>("lastModified"); - public static final QueryableStore.NumberQueryField HEIGHT = - new QueryableStore.NumberQueryField<>("height"); - public static final QueryableStore.NumberQueryField WIDTH = - new QueryableStore.NumberQueryField<>("width"); + public static final QueryableStore.FloatQueryField HEIGHT = + new QueryableStore.FloatQueryField<>("height"); + public static final QueryableStore.FloatQueryField WIDTH = + new QueryableStore.FloatQueryField<>("width"); - public static final QueryableStore.NumberQueryField PRICE = - new QueryableStore.NumberQueryField<>("price"); - public static final QueryableStore.NumberQueryField MARGIN = - new QueryableStore.NumberQueryField<>("margin"); + public static final QueryableStore.DoubleQueryField PRICE = + new QueryableStore.DoubleQueryField<>("price"); + public static final QueryableStore.DoubleQueryField MARGIN = + new QueryableStore.DoubleQueryField<>("margin"); private DQueryFields() { } diff --git a/scm-core/src/main/java/sonia/scm/plugin/ExtensionProcessor.java b/scm-core/src/main/java/sonia/scm/plugin/ExtensionProcessor.java index f94f7f2796..782854b11a 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ExtensionProcessor.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ExtensionProcessor.java @@ -80,7 +80,7 @@ public interface ExtensionProcessor /** * Returns all queryable types. - * @since 3.7.0 + * @since 3.8.0 */ default Iterable getQueryableTypes() { return emptySet(); diff --git a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java index 0109779257..045e23ec4d 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java +++ b/scm-core/src/main/java/sonia/scm/plugin/ScmModule.java @@ -116,7 +116,7 @@ public class ScmModule { } /** - * @since 3.7.0 + * @since 3.8.0 */ public Iterable getQueryableTypes() { return nonNull(queryableTypes); diff --git a/scm-core/src/main/java/sonia/scm/store/QueryableMutableStore.java b/scm-core/src/main/java/sonia/scm/store/QueryableMutableStore.java index e80aa6ccba..ec3794c86e 100644 --- a/scm-core/src/main/java/sonia/scm/store/QueryableMutableStore.java +++ b/scm-core/src/main/java/sonia/scm/store/QueryableMutableStore.java @@ -29,7 +29,7 @@ import java.util.function.BooleanSupplier; * processor for the annotated type. * * @param The type of the objects to query. - * @since 3.7.0 + * @since 3.8.0 */ public interface QueryableMutableStore extends DataStore, QueryableStore, AutoCloseable { void transactional(BooleanSupplier callback); diff --git a/scm-core/src/main/java/sonia/scm/store/QueryableStore.java b/scm-core/src/main/java/sonia/scm/store/QueryableStore.java index 01799dad26..8e2c9401a1 100644 --- a/scm-core/src/main/java/sonia/scm/store/QueryableStore.java +++ b/scm-core/src/main/java/sonia/scm/store/QueryableStore.java @@ -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 The type of the objects to query. - * @since 3.7.0 + * @since 3.8.0 */ public interface QueryableStore extends AutoCloseable { @@ -90,7 +91,9 @@ public interface QueryableStore extends AutoCloseable { /** * Returns all objects that match the query. If the query returns no result, an empty list will be returned. */ - List findAll(); + default List 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 extends AutoCloseable { */ List 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 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 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 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 min(AggregatableQueryField field); + + /** + * Returns the maximum value of the given field that match the query. + */ + A max(AggregatableQueryField field); + + /** + * Returns the sum of the given field that match the query. + */ + A sum(AggregatableNumberQueryField field); + + /** + * Returns the average value of the given field that match the query. + */ + Double average(AggregatableNumberQueryField field); } /** @@ -160,10 +201,17 @@ public interface QueryableStore extends AutoCloseable { * @param The type of the field. */ @SuppressWarnings("unused") - class QueryField { - final String name; + interface QueryField { + String getName(); - public QueryField(String name) { + boolean isIdField(); + } + + @SuppressWarnings("unused") + abstract class BaseQueryField implements QueryField { + private final String name; + + BaseQueryField(String name) { this.name = name; } @@ -185,6 +233,19 @@ public interface QueryableStore extends AutoCloseable { } } + /** + * Query fields implementing this can compute aggregates like minimum or maximum. + */ + interface AggregatableQueryField extends QueryField { + Class getFieldType(); + } + + /** + * Query fields implementing this can compute aggregates like sum or average. + */ + interface AggregatableNumberQueryField extends AggregatableQueryField { + } + /** * 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 extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class StringQueryField extends QueryField { + class StringQueryField extends BaseQueryField implements AggregatableQueryField { public StringQueryField(String name) { super(name); @@ -239,6 +300,11 @@ public interface QueryableStore extends AutoCloseable { public Condition in(Collection values) { return in(values.toArray(new String[0])); } + + @Override + public Class getFieldType() { + return String.class; + } } /** @@ -274,9 +340,9 @@ public interface QueryableStore extends AutoCloseable { * @param The type of the objects this condition is used for. * @param The type of the number field. */ - class NumberQueryField extends QueryField { + abstract class NumberQueryField extends BaseQueryField { - public NumberQueryField(String name) { + NumberQueryField(String name) { super(name); } @@ -362,6 +428,50 @@ public interface QueryableStore extends AutoCloseable { } } + class IntegerQueryField extends NumberQueryField implements AggregatableNumberQueryField { + public IntegerQueryField(String name) { + super(name); + } + + @Override + public Class getFieldType() { + return Integer.class; + } + } + + class LongQueryField extends NumberQueryField implements AggregatableNumberQueryField { + public LongQueryField(String name) { + super(name); + } + + @Override + public Class getFieldType() { + return Long.class; + } + } + + class FloatQueryField extends NumberQueryField implements AggregatableNumberQueryField { + public FloatQueryField(String name) { + super(name); + } + + @Override + public Class getFieldType() { + return Float.class; + } + } + + class DoubleQueryField extends NumberQueryField implements AggregatableNumberQueryField { + public DoubleQueryField(String name) { + super(name); + } + + @Override + public Class 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 extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class InstantQueryField extends QueryField { + class InstantQueryField extends BaseQueryField { public InstantQueryField(String name) { super(name); } @@ -467,7 +577,7 @@ public interface QueryableStore extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class BooleanQueryField extends QueryField { + class BooleanQueryField extends BaseQueryField { public BooleanQueryField(String name) { super(name); @@ -511,7 +621,7 @@ public interface QueryableStore extends AutoCloseable { * @param The type of the objects this condition is used for. * @param The type of the enum field. */ - class EnumQueryField> extends QueryField> { + class EnumQueryField> extends BaseQueryField> { public EnumQueryField(String name) { super(name); } @@ -556,7 +666,7 @@ public interface QueryableStore extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class CollectionQueryField extends QueryField { + class CollectionQueryField extends BaseQueryField { public CollectionQueryField(String name) { super(name); } @@ -582,7 +692,7 @@ public interface QueryableStore extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class CollectionSizeQueryField extends NumberQueryField { + class CollectionSizeQueryField extends NumberQueryField implements AggregatableNumberQueryField { public CollectionSizeQueryField(String name) { super(name); } @@ -595,6 +705,11 @@ public interface QueryableStore extends AutoCloseable { public Condition isEmpty() { return eq(0L); } + + @Override + public Class getFieldType() { + return Long.class; + } } /** @@ -606,7 +721,7 @@ public interface QueryableStore extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class MapQueryField extends QueryField { + class MapQueryField extends BaseQueryField { public MapQueryField(String name) { super(name); } @@ -642,7 +757,7 @@ public interface QueryableStore extends AutoCloseable { * * @param The type of the objects this condition is used for. */ - class MapSizeQueryField extends NumberQueryField { + class MapSizeQueryField extends NumberQueryField implements AggregatableNumberQueryField { public MapSizeQueryField(String name) { super(name); } @@ -655,6 +770,11 @@ public interface QueryableStore extends AutoCloseable { public Condition isEmpty() { return eq(0L); } + + @Override + public Class getFieldType() { + return Long.class; + } } /** diff --git a/scm-core/src/main/java/sonia/scm/store/QueryableStoreFactory.java b/scm-core/src/main/java/sonia/scm/store/QueryableStoreFactory.java index 8efe749b9d..6a3de83424 100644 --- a/scm-core/src/main/java/sonia/scm/store/QueryableStoreFactory.java +++ b/scm-core/src/main/java/sonia/scm/store/QueryableStoreFactory.java @@ -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 { diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/BadStoreNameException.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/BadStoreNameException.java index cc02ceac5f..9c8bfb58cb 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/BadStoreNameException.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/BadStoreNameException.java @@ -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) { diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLAggregate.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLAggregate.java new file mode 100644 index 0000000000..cb3b30937f --- /dev/null +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLAggregate.java @@ -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) + ") "); + } +} diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java index 37205f28e0..42f2830f98 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLCondition.java @@ -26,12 +26,10 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.time.Instant; -import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier; - /** * SQLCondition 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); } } diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLField.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLField.java index 965e92ede0..29e05435cf 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLField.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLField.java @@ -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 { diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLFieldHelper.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLFieldHelper.java new file mode 100644 index 0000000000..46ecd7f1e6 --- /dev/null +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLFieldHelper.java @@ -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() + "')"; + } + } +} diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLValue.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLValue.java index 6510378f8d..61022ed816 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLValue.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLValue.java @@ -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 { diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java index 1da7a2cffe..af152b3c52 100644 --- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java +++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java @@ -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 implements QueryableStore, QueryableMaintenance } @Override - public List findAll() { - return findAll(0, Integer.MAX_VALUE); + public List findAll(long offset, long limit) { + List result = new ArrayList<>(); + forEach(result::add, offset, limit); + return Collections.unmodifiableList(result); } @Override - public List findAll(long offset, long limit) { + public void forEach(Consumer consumer, long offset, long limit) { StringBuilder orderByBuilder = new StringBuilder(); if (orderBy != null && !orderBy.isEmpty()) { toOrderBySQL(orderByBuilder); @@ -326,15 +329,15 @@ class SQLiteQueryableStore implements QueryableStore, QueryableMaintenance offset ); - return executeRead( + executeRead( sqlSelectQuery, statement -> { - List 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 implements QueryableStore, QueryableMaintenance ); } + @Override + public A min(AggregatableQueryField field) { + return aggregate(field, "MIN", field.getFieldType()); + } + + @Override + public A max(AggregatableQueryField field) { + return aggregate(field, "MAX", field.getFieldType()); + } + + @Override + public A sum(AggregatableNumberQueryField field) { + return aggregate(field, "SUM", field.getFieldType()); + } + + @Override + public Double average(AggregatableNumberQueryField field) { + return aggregate(field, "AVG", Double.class); + } + + private A aggregate(AggregatableQueryField field, String aggregate, Class 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 orderBy(QueryField field, Order order) { List> extendedOrderBy = new ArrayList<>(this.orderBy); diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java index 0dbf0fa299..54e306fe3b 100644 --- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java +++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableStoreTest.java @@ -64,7 +64,7 @@ class SQLiteQueryableStoreTest { store.put(new Spaceship("Heart Of Gold", Range.INTER_GALACTIC)); List 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 all = store.query( - CREATION_DATE_QUERY_FIELD.lessOrEquals(9999999999L) + CREATION_DATE.lessOrEquals(9999999999L) ) .findAll(); @@ -102,7 +102,7 @@ class SQLiteQueryableStoreTest { store.put(arthur); List 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 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 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 store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class); + Integer result = store.query().max( + SPACESHIP_FLIGHT_COUNT + ); + + assertThat(result).isNull(); + } + + @Nested + class ForAggregations { + + SQLiteQueryableMutableStore 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 store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class); @@ -199,20 +283,20 @@ class SQLiteQueryableStoreTest { store.put(new Spaceship("MillenniumFalcon")); List 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 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 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 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 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 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 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 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 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 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 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 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 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 all = store.query( - ID_QUERY_FIELD.eq("1") + ID.eq("1") ) .findAll(); @@ -385,7 +469,7 @@ class SQLiteQueryableStoreTest { SQLiteQueryableStore store = new StoreTestBuilder(connectionString, Group.class.getName()).withIds(); List 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 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 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 all = store.query( - Conditions.not(DISPLAY_NAME_QUERY_FIELD.isNull()) + Conditions.not(DISPLAY_NAME.isNull()) ) .findAll(); @@ -450,8 +534,8 @@ class SQLiteQueryableStoreTest { List 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 all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll(); + List 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 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 all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll(); + List 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 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 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 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 = 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 = 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 = 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 store = new StoreTestBuilder(connectionString).withIds(); Optional 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 ID_QUERY_FIELD = + private static final QueryableStore.IdQueryField ID = new QueryableStore.IdQueryField<>(); - private static final QueryableStore.IdQueryField GROUP_QUERY_FIELD = + private static final QueryableStore.IdQueryField GROUP = new QueryableStore.IdQueryField<>(Group.class); - private static final QueryableStore.StringQueryField USER_NAME_QUERY_FIELD = + private static final QueryableStore.StringQueryField USER_NAME = new QueryableStore.StringQueryField<>("name"); - private static final QueryableStore.StringQueryField DISPLAY_NAME_QUERY_FIELD = + private static final QueryableStore.StringQueryField DISPLAY_NAME = new QueryableStore.StringQueryField<>("displayName"); - private static final QueryableStore.StringQueryField MAIL_QUERY_FIELD = + private static final QueryableStore.StringQueryField MAIL = new QueryableStore.StringQueryField<>("mail"); - private static final QueryableStore.NumberQueryField CREATION_DATE_QUERY_FIELD = - new QueryableStore.NumberQueryField<>("creationDate"); - private static final QueryableStore.NumberQueryField CREATION_DATE_AS_INTEGER_QUERY_FIELD = - new QueryableStore.NumberQueryField<>("creationDate"); - private static final QueryableStore.BooleanQueryField ACTIVE_QUERY_FIELD = + private static final QueryableStore.LongQueryField CREATION_DATE = + new QueryableStore.LongQueryField<>("creationDate"); + private static final QueryableStore.IntegerQueryField CREATION_DATE_AS_INTEGER = + new QueryableStore.IntegerQueryField<>("creationDate"); + private static final QueryableStore.BooleanQueryField ACTIVE = new QueryableStore.BooleanQueryField<>("active"); enum Range { SOLAR_SYSTEM, INNER_GALACTIC, INTER_GALACTIC } - private static final QueryableStore.StringQueryField SPACESHIP_NAME_QUERY_FIELD = + private static final QueryableStore.StringQueryField SPACESHIP_ID = + new QueryableStore.IdQueryField<>(); + private static final QueryableStore.StringQueryField SPACESHIP_NAME = new QueryableStore.StringQueryField<>("name"); - private static final QueryableStore.EnumQueryField SPACESHIP_RANGE_ENUM_QUERY_FIELD = + private static final QueryableStore.EnumQueryField SPACESHIP_RANGE = new QueryableStore.EnumQueryField<>("range"); - private static final QueryableStore.CollectionQueryField SPACESHIP_CREW_QUERY_FIELD = + private static final QueryableStore.CollectionQueryField SPACESHIP_CREW = new QueryableStore.CollectionQueryField<>("crew"); - private static final QueryableStore.CollectionSizeQueryField SPACESHIP_CREW_SIZE_QUERY_FIELD = + private static final QueryableStore.CollectionSizeQueryField SPACESHIP_CREW_SIZE = new QueryableStore.CollectionSizeQueryField<>("crew"); - private static final QueryableStore.MapQueryField SPACESHIP_DESTINATIONS_QUERY_FIELD = + private static final QueryableStore.MapQueryField SPACESHIP_DESTINATIONS = new QueryableStore.MapQueryField<>("destinations"); - private static final QueryableStore.MapSizeQueryField SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD = + private static final QueryableStore.MapSizeQueryField SPACESHIP_DESTINATIONS_SIZE = new QueryableStore.MapSizeQueryField<>("destinations"); - private static final QueryableStore.InstantQueryField SPACESHIP_INSERVICE_QUERY_FIELD = + private static final QueryableStore.InstantQueryField SPACESHIP_INSERVICE = new QueryableStore.InstantQueryField<>("inServiceSince"); + private static final QueryableStore.IntegerQueryField SPACESHIP_FLIGHT_COUNT = + new QueryableStore.IntegerQueryField<>("flightCount"); } diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/Spaceship.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/Spaceship.java index 7241f37b99..8356e24362 100644 --- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/Spaceship.java +++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/Spaceship.java @@ -37,6 +37,7 @@ class Spaceship { Collection crew; Map 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; + } }