Introduce retain and deleteAll for queryable stores

This commit is contained in:
Till-André Diegeler
2025-06-10 15:59:31 +02:00
parent c262038d22
commit 60b672cf59
12 changed files with 562 additions and 83 deletions

View File

@@ -91,6 +91,22 @@ extends `QueryableStore` and `DataStore` to allow both queries and changes to st
pure Queryable Store, it is mandatory to specify all parents to create a mutable store. This is needed so that new
entities can be assigned to the correct parent(s).
Deleting objects in mutual stores can be done by either selecting all elements to be deleted with a query and
execute the `deleteAll()` function of the API or using a `retain(long keptElements)`. The latter is the recommended way
to implement FIFO *(first in, first out)* lists, which is especially intended for managing log entries.
Take this example: You are maintaining a display of when your spaceships received their maintenance.
```java
spaceshipStore
.query(Spaceship.SPACESHIP_INSERVICE
.after(Instant.now().minus(5, ChronoUnit.DAYS)))
.orderBy(Order.DESC)
.retain(10);
```
With this code snippet, you will delete all alements older than five days and only keep 10 newest ones
matching this criterion.
### Queryable Maintenance Store
The `QueryableMaintenanceStore` is responsible for maintenance tasks,

View File

@@ -0,0 +1,2 @@
- type: added
description: Delete and retain functionality for mutable queryable stores

View File

@@ -34,6 +34,55 @@ import java.util.function.BooleanSupplier;
public interface QueryableMutableStore<T> extends DataStore<T>, QueryableStore<T>, AutoCloseable {
void transactional(BooleanSupplier callback);
@Override
MutableQuery<T, ?> query(Condition<T>... conditions);
@Override
void close();
/**
*
* @param <T> "Type" &ndash; type of the objects to query
* @param <S> "Self" &ndash; specification of the {@link MutableQuery}.
*/
interface MutableQuery<T, S extends MutableQuery<T, S>> extends Query<T, T, S> {
/**
* Deletes all entries except the {@code keptElements} highest ones in terms of the provided order and query.
* <br/><br/>
* For example, calling {@code retain(4)} after a query
* {@code store.query(CREATION_TIME.after(Instant.now().minus(5, DAYS)).orderBy(Order.DESC)} will remove every entry
* except those that
* <ul>
* <li>Match any conditions given by preceding queries (here: {@code store.query(...)} saying that only elements
* newer than five days may be kept), and</li>
* <li>Are among the 4 first ones in terms of the order given by the query result (here: a descending order with
* the newest being first).</li>
* </ul>
* This function is expected to only work in the realm of the {@link QueryableMutableStore}. For example, elements with
* other parent ids in some implementations are supposed to remain unaffected.
* <br/><br/>
* <em>Note:</em> {@link #deleteAll()} is identical to {@code retain(0)}.
* @param keptElements Quantity of entities to be retained.
* @since 3.9.0
*/
void retain(long keptElements);
/**
* Deletes all entries matching the given query conditions.
* <br/><br/>
* For example, calling {@code deleteAll()} after a query
* {@code store.query(CREATION_TIME.before(Instant.now().minus(5, DAYS))} will remove every entry with a
* {@code CREATION_TIME} property older than five days and keep those that don't match this condition (newer date).
* <br/>
* If no conditions have been selected beforehand, all entries in the realm of this {@link QueryableMutableStore}
* instance will be removed. It does not affect the structure of the store, and new entries may be added afterwards.
* <br/><br/>
* This function is expected to only work in the realm of the {@link QueryableMutableStore}. For example, elements with
* other parent ids are supposed to remain unaffected.
* <br/><br/>
* <em>Note:</em> Consider {@link #retain(long)} if you prefer to deliberately keep a given amounts of elements instead.
* @since 3.9.0
*/
void deleteAll();
}
}

View File

@@ -50,7 +50,7 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param conditions The conditions to filter the objects.
* @return The query object to retrieve the result.
*/
Query<T, T> query(Condition<T>... conditions);
Query<T, T, ?> query(Condition<T>... conditions);
/**
* Used to specify the order of the result of a query.
@@ -74,7 +74,7 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param <T_RESULT> The type of the result objects (if a projection had been made, for example using
* {@link #withIds()}).
*/
interface Query<T, T_RESULT> {
interface Query<T, T_RESULT, SELF extends Query<T, T_RESULT, SELF>> {
/**
* Returns the first found object, if the query returns at least one result.
@@ -129,7 +129,7 @@ public interface QueryableStore<T> extends AutoCloseable {
*
* @return The query object to continue building the query.
*/
Query<T, Result<T_RESULT>> withIds();
Query<T, Result<T_RESULT>, ?> withIds();
/**
* Orders the result by the given field in the given order. If the order is not set, the order of the result is not
@@ -139,7 +139,7 @@ public interface QueryableStore<T> extends AutoCloseable {
* @param order The order to use (either ascending or descending).
* @return The query object to continue building the query.
*/
Query<T, T_RESULT> orderBy(QueryField<T, ?> field, Order order);
SELF orderBy(QueryField<T, ?> field, Order order);
/**
* Returns the count of all objects that match the query.

View File

@@ -33,6 +33,9 @@ class SQLSelectStatement extends ConditionalSQLStatement {
SQLSelectStatement(List<SQLField> columns, SQLTable fromTable, List<SQLNodeWithValue> whereCondition, String orderBy, long limit, long offset) {
super(whereCondition);
if (limit < 0 || offset < 0) {
throw new IllegalArgumentException("limit and offset must be non-negative");
}
this.columns = columns;
this.fromTable = fromTable;
this.orderBy = orderBy;

View File

@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.plugin.QueryableTypeDescriptor;
import sonia.scm.security.KeyGenerator;
import sonia.scm.store.Condition;
import sonia.scm.store.IdHandlerForStores;
import sonia.scm.store.QueryableMutableStore;
import sonia.scm.store.StoreException;
@@ -157,6 +158,11 @@ class SQLiteQueryableMutableStore<T> extends SQLiteQueryableStore<T> implements
);
}
@Override
public MutableQuery<T, ?> query(Condition<T>... conditions) {
return new SQLiteMutableQuery(clazz, conditions);
}
private String marshal(T item) {
try {
return objectMapper.writeValueAsString(item);
@@ -170,4 +176,77 @@ class SQLiteQueryableMutableStore<T> extends SQLiteQueryableStore<T> implements
conditions.add(new SQLCondition("=", new SQLField("ID"), new SQLValue(id)));
return conditions;
}
private class SQLiteMutableQuery extends SQLiteQuery<T, SQLiteMutableQuery> implements MutableQuery<T, SQLiteMutableQuery>, Cloneable {
SQLiteMutableQuery(Class<T> type, Condition<T>[] conditions) {
super(type, conditions);
}
@Override
public void deleteAll() {
List<SQLNodeWithValue> parentConditions = new ArrayList<>();
evaluateParentConditions(parentConditions);
SQLDeleteStatement sqlStatementQuery =
new SQLDeleteStatement(
computeFromTable(),
computeCondition()
);
executeWrite(
sqlStatementQuery,
statement -> {
statement.executeUpdate();
return null;
}
);
log.debug("All entries for {} have been deleted.", SQLiteQueryableMutableStore.this);
}
@Override
public void retain(long n) {
List<SQLField> columns = new ArrayList<>();
addParentIdSQLFields(columns);
List<SQLNodeWithValue> conditions = new ArrayList<>();
List<SQLNodeWithValue> parentConditions = new ArrayList<>();
evaluateParentConditions(parentConditions);
conditions.addAll(parentConditions);
conditions.addAll(this.computeCondition());
SQLSelectStatement selectStatement = new SQLSelectStatement(
columns,
computeFromTable(),
conditions,
getOrderByString(),
n,
0L
);
SQLiteRetainStatement retainStatement = new SQLiteRetainStatement(
computeFromTable(),
columns,
selectStatement,
parentConditions
);
executeWrite(
retainStatement,
statement -> {
statement.executeUpdate();
return null;
}
);
log.debug("All entries for {} have been deleted retaining the {} highest ones by ordering.", SQLiteQueryableMutableStore.this, n);
}
@Override
public SQLiteMutableQuery clone() {
return (SQLiteMutableQuery) super.clone();
}
}
}

View File

@@ -18,6 +18,9 @@ package sonia.scm.store.sqlite;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.plugin.QueryableTypeDescriptor;
import sonia.scm.store.Condition;
@@ -51,6 +54,7 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static java.lang.String.format;
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier;
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeTableName;
@@ -82,7 +86,7 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
}
@Override
public Query<T, T> query(Condition<T>... conditions) {
public Query<T, T, ?> query(Condition<T>... conditions) {
return new SQLiteQuery<>(clazz, conditions);
}
@@ -269,14 +273,67 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
return connection;
}
private class SQLiteQuery<T_RESULT> implements Query<T, T_RESULT> {
void evaluateParentConditions(List<SQLNodeWithValue> conditions) {
for (int i = 0; i < parentIds.length; i++) {
SQLCondition condition = new SQLCondition("=", new SQLField(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i])), new SQLValue(parentIds[i]));
conditions.add(condition);
}
}
void addParentIdSQLFields(List<SQLField> fields) {
for (int i = 0; i < queryableTypeDescriptor.getTypes().length; i++) {
fields.add(new SQLField(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i])));
}
fields.add(new SQLField("ID"));
}
private String serialize(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new SerializationException("failed to serialize object to json", e);
}
}
@Override
public String toString() {
return format("Store for class %s with parent ids %s", this.clazz.getName(), Arrays.toString(this.parentIds));
}
interface StatementCallback<R> {
R apply(PreparedStatement statement) throws SQLException, JsonProcessingException;
}
private interface RowBuilder<R> {
R build(String[] parentIds, String id, String json) throws JsonProcessingException;
}
record OrderBy<T>(QueryField<T, ?> field, Order order) {
@Override
public String toString() {
if (order == null) {
return field.getName();
} else {
return field.getName() + " " + order;
}
}
}
/**
* @param <T_RESULT> "Result" &ndash; result type
* @param <SELF> "Self" &ndash; instance type of this query
*/
@Setter(AccessLevel.PACKAGE)
@Getter(AccessLevel.PROTECTED)
class SQLiteQuery<T_RESULT, SELF extends SQLiteQuery<T_RESULT, SELF>> implements Query<T, T_RESULT, SELF>, Cloneable {
private final Class<T_RESULT> resultType;
private final Class<T> entityType;
private final Condition<T> condition;
private final List<OrderBy<T>> orderBy;
private List<OrderBy<T>> orderBy;
private SQLiteQuery(Class<T_RESULT> resultType, Condition<T>[] conditions) {
SQLiteQuery(Class<T_RESULT> resultType, Condition<T>[] conditions) {
this(resultType, resultType, conjunct(conditions), Collections.emptyList());
}
@@ -288,6 +345,16 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
this.orderBy = orderBy;
}
private static <T> Condition<T> conjunct(Condition<T>[] conditions) {
if (conditions.length == 0) {
return null;
} else if (conditions.length == 1) {
return conditions[0];
} else {
return Conditions.and(conditions);
}
}
@Override
public Optional<T_RESULT> findFirst() {
return findAll(0, 1).stream().findFirst();
@@ -314,17 +381,14 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
@Override
public void forEach(Consumer<T_RESULT> consumer, long offset, long limit) {
StringBuilder orderByBuilder = new StringBuilder();
if (orderBy != null && !orderBy.isEmpty()) {
toOrderBySQL(orderByBuilder);
}
String orderByString = getOrderByString();
SQLSelectStatement sqlSelectQuery =
new SQLSelectStatement(
computeFields(),
computeFromTable(),
computeCondition(),
orderByBuilder.toString(),
orderByString,
limit,
offset
);
@@ -342,9 +406,17 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
);
}
String getOrderByString() {
StringBuilder orderByBuilder = new StringBuilder();
if (orderBy != null && !orderBy.isEmpty()) {
toOrderBySQL(orderByBuilder);
}
return orderByBuilder.toString();
}
@Override
@SuppressWarnings("unchecked")
public Query<T, Result<T_RESULT>> withIds() {
public Query<T, Result<T_RESULT>, ?> withIds() {
return new SQLiteQuery<>((Class<Result<T_RESULT>>) (Class<?>) Result.class, resultType, condition, orderBy);
}
@@ -413,10 +485,12 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
}
@Override
public Query<T, T_RESULT> orderBy(QueryField<T, ?> field, Order order) {
public SELF orderBy(QueryField<T, ?> field, Order order) {
List<OrderBy<T>> extendedOrderBy = new ArrayList<>(this.orderBy);
extendedOrderBy.add(new OrderBy<>(field, order));
return new SQLiteQuery<>(resultType, entityType, condition, extendedOrderBy);
SELF newOrderBy = (SELF) this.clone();
newOrderBy.setOrderBy(extendedOrderBy);
return newOrderBy;
}
private List<SQLField> computeFields() {
@@ -428,20 +502,19 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
return fields;
}
private List<SQLNodeWithValue> computeCondition() {
List<SQLNodeWithValue> computeCondition() {
List<SQLNodeWithValue> conditions = new ArrayList<>();
evaluateParentConditions(conditions);
if (condition != null) {
if (condition instanceof LeafCondition<T, ?> leafCondition) {
SQLCondition sqlCondition = SQLConditionMapper.mapToSQLCondition(leafCondition);
conditions.add(sqlCondition);
conditions.add(SQLConditionMapper.mapToSQLCondition(leafCondition));
}
if (condition instanceof LogicalCondition<T> logicalCondition) {
SQLLogicalCondition sqlLogicalCondition = SQLConditionMapper.mapToSQLLogicalCondition(logicalCondition);
conditions.add(sqlLogicalCondition);
conditions.add(SQLConditionMapper.mapToSQLLogicalCondition(logicalCondition));
}
log.debug("Unsupported condition type: {}", condition.getClass().getName());
}
return conditions;
@@ -467,16 +540,16 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
private T_RESULT extractResult(ResultSet resultSet) throws JsonProcessingException, SQLException {
T entity = objectMapper.readValue(resultSet.getString(1), entityType);
if (resultType.isAssignableFrom(Result.class)) {
Map<String, String> parentIds = new HashMap<>(queryableTypeDescriptor.getTypes().length);
Map<String, String> parentIdMapping = new HashMap<>(queryableTypeDescriptor.getTypes().length);
for (int i = 0; i < queryableTypeDescriptor.getTypes().length; i++) {
parentIds.put(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i]), resultSet.getString(i + 2));
parentIdMapping.put(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i]), resultSet.getString(i + 2));
}
String id = resultSet.getString(queryableTypeDescriptor.getTypes().length + 2);
return (T_RESULT) new Result<T>() {
@Override
public Optional<String> getParentId(Class<?> clazz) {
String parentClassName = computeColumnIdentifier(clazz.getName());
return Optional.ofNullable(parentIds.get(parentClassName));
return Optional.ofNullable(parentIdMapping.get(parentClassName));
}
@Override
@@ -494,38 +567,21 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
}
}
private static <T> Condition<T> conjunct(Condition<T>[] conditions) {
if (conditions.length == 0) {
return null;
} else if (conditions.length == 1) {
return conditions[0];
} else {
return Conditions.and(conditions);
/* We explicitly suppress this warning since it's based on a generic whose information is lost during runtime,
which can be conveniently circumvented with clone().
*/
@SuppressWarnings("java:S2975")
@Override
public SQLiteQuery<T_RESULT, SELF> clone() {
try {
// Keep in mind that this clone shares the mutable entities with its origin.
return (SQLiteQuery<T_RESULT, SELF>) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
private void evaluateParentConditions(List<SQLNodeWithValue> conditions) {
for (int i = 0; i < parentIds.length; i++) {
SQLCondition condition = new SQLCondition("=", new SQLField(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i])), new SQLValue(parentIds[i]));
conditions.add(condition);
}
}
private void addParentIdSQLFields(List<SQLField> fields) {
for (int i = 0; i < queryableTypeDescriptor.getTypes().length; i++) {
fields.add(new SQLField(computeColumnIdentifier(queryableTypeDescriptor.getTypes()[i])));
}
fields.add(new SQLField("ID"));
}
interface StatementCallback<R> {
R apply(PreparedStatement statement) throws SQLException, JsonProcessingException;
}
record OrderBy<T>(QueryField<T, ?> field, Order order) {
}
private class TemporaryTableMaintenanceIterator implements MaintenanceIterator<T> {
private final PreparedStatement iterateStatement;
private final List<SQLField> columns;
@@ -741,16 +797,4 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
}
}
}
private String serialize(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new SerializationException("failed to serialize object to json", e);
}
}
private interface RowBuilder<R> {
R build(String[] parentIds, String id, String json) throws JsonProcessingException;
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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 java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.util.stream.Collectors;
import static java.lang.String.format;
class SQLiteRetainStatement implements SQLNodeWithValue {
private final SQLTable table;
private final List<SQLField> columns;
private final SQLSelectStatement selectStatement;
private final List<SQLNodeWithValue> parentConditions;
SQLiteRetainStatement(SQLTable table, List<SQLField> columns, SQLSelectStatement selectStatement, List<SQLNodeWithValue> parentConditions) {
this.table = table;
this.columns = columns;
this.selectStatement = selectStatement;
this.parentConditions = parentConditions;
}
@Override
public int apply(PreparedStatement statement, int index) throws SQLException {
index = selectStatement.apply(statement, index);
for (SQLNodeWithValue condition : parentConditions) {
index = condition.apply(statement, index);
}
return index;
}
@Override
public String toSQL() {
String parentConditionStatement;
if (parentConditions == null || parentConditions.isEmpty()) {
parentConditionStatement = "";
} else {
parentConditionStatement = "AND " + new SQLLogicalCondition("AND", parentConditions).toSQL();
}
return format("DELETE FROM %s WHERE (%s) NOT IN (%s) %s",
table.toSQL(),
columns.stream().map(SQLField::toSQL).collect(Collectors.joining(",")),
selectStatement.toSQL(),
parentConditionStatement);
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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 lombok.Data;
import lombok.NoArgsConstructor;
import sonia.scm.store.QueryableStore;
import sonia.scm.store.QueryableType;
@Data
@QueryableType({Spaceship.class})
@NoArgsConstructor
class Crewmate {
static final QueryableStore.StringQueryField<Spaceship> CREWMATE_ID = new QueryableStore.IdQueryField<>();
Spaceship spaceship;
String name;
String description;
Crewmate(Spaceship spaceship) {
this.spaceship = spaceship;
}
}

View File

@@ -21,6 +21,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.store.QueryableMutableStore;
import sonia.scm.store.QueryableStore;
import sonia.scm.user.User;
import java.nio.file.Path;
@@ -28,9 +30,11 @@ import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SuppressWarnings("resource")
class SQLiteQueryableMutableStoreTest {
@@ -309,4 +313,174 @@ class SQLiteQueryableMutableStoreTest {
assertThat(store.getAll()).containsOnlyKeys("tricia");
}
}
@Nested
class DeleteAll {
@Test
void shouldDeleteAllInStoreWithoutSubsequentQuery() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
store.query()
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.ASC)
.deleteAll();
assertThat(store.getAll()).isEmpty();
}
@Test
void shouldOnlyDeleteElementsMatchingTheQuery() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
store.query(Spaceship.SPACESHIP_ID.in("1", "3"))
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.ASC)
.deleteAll();
assertThat(store.getAll()).hasSize(1);
assertThat(store.getAll()).containsOnlyKeys("2");
}
@Test
void shouldKeepEntriesFromOtherStore() {
StoreTestBuilder spaceshipStoreBuilder = new StoreTestBuilder(connectionString);
StoreTestBuilder crewmateStoreBuilder = new StoreTestBuilder(connectionString, "Spaceship");
try (
SQLiteQueryableMutableStore<Spaceship> spaceshipStore = spaceshipStoreBuilder.forClassWithIds(Spaceship.class);
SQLiteQueryableMutableStore<Crewmate> crewmateStoreForShipOne = crewmateStoreBuilder.forClassWithIds(Crewmate.class, "1");
SQLiteQueryableMutableStore<Crewmate> crewmateStoreForShipTwo = crewmateStoreBuilder.forClassWithIds(Crewmate.class, "2")
) {
Spaceship spaceshipOne = new Spaceship("1");
Spaceship spaceshipTwo = new Spaceship("2");
spaceshipStore.put(spaceshipOne);
spaceshipStore.put(spaceshipTwo);
crewmateStoreForShipOne.put("1", new Crewmate(spaceshipOne));
crewmateStoreForShipOne.put("2", new Crewmate(spaceshipOne));
crewmateStoreForShipTwo.put("1", new Crewmate(spaceshipTwo));
crewmateStoreForShipOne.query().deleteAll();
assertThat(crewmateStoreForShipOne.getAll()).isEmpty();
assertThat(crewmateStoreForShipTwo.getAll()).hasSize(1);
}
}
}
@Nested
class Retain {
@Test
void shouldRetainOneWithAscendingOrder() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
store.query()
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.ASC)
.retain(1);
assertThat(store.getAll()).hasSize(1);
assertThat(store.get("1")).isNotNull();
}
@Test
void shouldThrowIllegalArgumentExceptionIfKeptElementsIsNegative() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
QueryableMutableStore.MutableQuery mutableQuery = store.query()
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.ASC);
assertThatThrownBy(() -> mutableQuery.retain(-1)).isInstanceOf(IllegalArgumentException.class);
}
@Test
void shouldRetainOneWithDescendingOrder() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
store.query()
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.DESC)
.retain(1);
assertThat(store.getAll()).hasSize(1);
assertThat(store.get("3")).isNotNull();
}
@Test
void shouldDeleteUnselectedEntitiesAndRetainKeptElementsFromTheSelectedOnes() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship spaceshipOne = new Spaceship("LazyShip");
Spaceship spaceshipTwo = new Spaceship("Biblical Ship");
Spaceship spaceshipThree = new Spaceship("Millennium");
spaceshipOne.crew = List.of("Foxtrot", "Icebear", "Possum");
spaceshipTwo.crew = List.of("Adam", "Eva", "Gabriel", "Lilith", "Michael");
spaceshipThree.crew = List.of("Chewbacca", "R2-D2", "C3PO", "Han Solo", "Luke Skywalker", "Obi-Wan Kenobi");
store.put("LazyShip", spaceshipOne);
store.put("Biblical Ship", spaceshipTwo);
store.put("Millennium", spaceshipThree);
store.query(Spaceship.SPACESHIP_CREW_SIZE.greater(3L))
.orderBy(Spaceship.SPACESHIP_CREW_SIZE, QueryableStore.Order.DESC)
.retain(1);
assertThat(store.getAll()).hasSize(1);
assertThat(store.get("LazyShip")).isNull();
assertThat(store.get("Biblical Ship")).isNull();
assertThat(store.get("Millennium")).isNotNull();
}
@Test
void shouldRetainEverythingIfKeptElementsHigherThanContentQuantity() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put("1", new Spaceship("1"));
store.put("2", new Spaceship("2"));
store.put("3", new Spaceship("3"));
store.query()
.orderBy(Spaceship.SPACESHIP_NAME, QueryableStore.Order.DESC)
.retain(5);
assertThat(store.getAll()).hasSize(3);
}
@Test
void shouldKeepEntriesFromOtherStore() {
StoreTestBuilder spaceshipStoreBuilder = new StoreTestBuilder(connectionString);
StoreTestBuilder crewmateStoreBuilder = new StoreTestBuilder(connectionString, "Spaceship");
try (
SQLiteQueryableMutableStore<Spaceship> spaceshipStore = spaceshipStoreBuilder.forClassWithIds(Spaceship.class);
SQLiteQueryableMutableStore<Crewmate> crewmateStoreForShipOne = crewmateStoreBuilder.forClassWithIds(Crewmate.class, "1");
SQLiteQueryableMutableStore<Crewmate> crewmateStoreForShipTwo = crewmateStoreBuilder.forClassWithIds(Crewmate.class, "2")
) {
Spaceship spaceshipOne = new Spaceship("1");
Spaceship spaceshipTwo = new Spaceship("2");
spaceshipStore.put(spaceshipOne);
spaceshipStore.put(spaceshipTwo);
crewmateStoreForShipOne.put("1", new Crewmate(spaceshipOne));
crewmateStoreForShipOne.put("2", new Crewmate(spaceshipOne));
crewmateStoreForShipTwo.put("1", new Crewmate(spaceshipTwo));
crewmateStoreForShipTwo.put("2", new Crewmate(spaceshipTwo));
crewmateStoreForShipOne.query().retain(1);
assertThat(crewmateStoreForShipOne.getAll()).hasSize(1);
assertThat(crewmateStoreForShipTwo.getAll()).hasSize(2);
}
}
}
}

View File

@@ -40,6 +40,15 @@ import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_CREW;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_CREW_SIZE;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_DESTINATIONS;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_DESTINATIONS_SIZE;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_FLIGHT_COUNT;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_ID;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_INSERVICE;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_NAME;
import static sonia.scm.store.sqlite.Spaceship.SPACESHIP_RANGE;
@SuppressWarnings({"resource", "unchecked"})
class SQLiteQueryableStoreTest {
@@ -1036,23 +1045,4 @@ class SQLiteQueryableStoreTest {
enum Range {
SOLAR_SYSTEM, INNER_GALACTIC, INTER_GALACTIC
}
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 =
new QueryableStore.EnumQueryField<>("range");
private static final QueryableStore.CollectionQueryField<Spaceship> SPACESHIP_CREW =
new QueryableStore.CollectionQueryField<>("crew");
private static final QueryableStore.CollectionSizeQueryField<Spaceship> SPACESHIP_CREW_SIZE =
new QueryableStore.CollectionSizeQueryField<>("crew");
private static final QueryableStore.MapQueryField<Spaceship> SPACESHIP_DESTINATIONS =
new QueryableStore.MapQueryField<>("destinations");
private static final QueryableStore.MapSizeQueryField<Spaceship> SPACESHIP_DESTINATIONS_SIZE =
new QueryableStore.MapSizeQueryField<>("destinations");
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

@@ -20,6 +20,7 @@ import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.EqualsAndHashCode;
import sonia.scm.store.QueryableStore;
import sonia.scm.store.QueryableType;
import java.time.Instant;
@@ -32,6 +33,26 @@ import java.util.Map;
@QueryableType
@EqualsAndHashCode
class Spaceship {
static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_ID =
new QueryableStore.IdQueryField<>();
static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_NAME =
new QueryableStore.StringQueryField<>("name");
static final QueryableStore.EnumQueryField<Spaceship, SQLiteQueryableStoreTest.Range> SPACESHIP_RANGE =
new QueryableStore.EnumQueryField<>("range");
static final QueryableStore.CollectionQueryField<Spaceship> SPACESHIP_CREW =
new QueryableStore.CollectionQueryField<>("crew");
static final QueryableStore.CollectionSizeQueryField<Spaceship> SPACESHIP_CREW_SIZE =
new QueryableStore.CollectionSizeQueryField<>("crew");
static final QueryableStore.MapQueryField<Spaceship> SPACESHIP_DESTINATIONS =
new QueryableStore.MapQueryField<>("destinations");
static final QueryableStore.MapSizeQueryField<Spaceship> SPACESHIP_DESTINATIONS_SIZE =
new QueryableStore.MapSizeQueryField<>("destinations");
static final QueryableStore.InstantQueryField<Spaceship> SPACESHIP_INSERVICE =
new QueryableStore.InstantQueryField<>("inServiceSince");
static final QueryableStore.IntegerQueryField<Spaceship> SPACESHIP_FLIGHT_COUNT =
new QueryableStore.IntegerQueryField<>("flightCount");
String name;
SQLiteQueryableStoreTest.Range range;
Collection<String> crew;