mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 11:05:56 +01:00
Introduce retain and deleteAll for queryable stores
This commit is contained in:
@@ -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
|
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).
|
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
|
### Queryable Maintenance Store
|
||||||
|
|
||||||
The `QueryableMaintenanceStore` is responsible for maintenance tasks,
|
The `QueryableMaintenanceStore` is responsible for maintenance tasks,
|
||||||
|
|||||||
2
gradle/changelog/extend_sqlite_api.yaml
Normal file
2
gradle/changelog/extend_sqlite_api.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Delete and retain functionality for mutable queryable stores
|
||||||
@@ -34,6 +34,55 @@ import java.util.function.BooleanSupplier;
|
|||||||
public interface QueryableMutableStore<T> extends DataStore<T>, QueryableStore<T>, AutoCloseable {
|
public interface QueryableMutableStore<T> extends DataStore<T>, QueryableStore<T>, AutoCloseable {
|
||||||
void transactional(BooleanSupplier callback);
|
void transactional(BooleanSupplier callback);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
MutableQuery<T, ?> query(Condition<T>... conditions);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T> "Type" – type of the objects to query
|
||||||
|
* @param <S> "Self" – 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public interface QueryableStore<T> extends AutoCloseable {
|
|||||||
* @param conditions The conditions to filter the objects.
|
* @param conditions The conditions to filter the objects.
|
||||||
* @return The query object to retrieve the result.
|
* @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.
|
* 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
|
* @param <T_RESULT> The type of the result objects (if a projection had been made, for example using
|
||||||
* {@link #withIds()}).
|
* {@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.
|
* 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.
|
* @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
|
* 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).
|
* @param order The order to use (either ascending or descending).
|
||||||
* @return The query object to continue building the query.
|
* @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.
|
* Returns the count of all objects that match the query.
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ class SQLSelectStatement extends ConditionalSQLStatement {
|
|||||||
|
|
||||||
SQLSelectStatement(List<SQLField> columns, SQLTable fromTable, List<SQLNodeWithValue> whereCondition, String orderBy, long limit, long offset) {
|
SQLSelectStatement(List<SQLField> columns, SQLTable fromTable, List<SQLNodeWithValue> whereCondition, String orderBy, long limit, long offset) {
|
||||||
super(whereCondition);
|
super(whereCondition);
|
||||||
|
if (limit < 0 || offset < 0) {
|
||||||
|
throw new IllegalArgumentException("limit and offset must be non-negative");
|
||||||
|
}
|
||||||
this.columns = columns;
|
this.columns = columns;
|
||||||
this.fromTable = fromTable;
|
this.fromTable = fromTable;
|
||||||
this.orderBy = orderBy;
|
this.orderBy = orderBy;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import sonia.scm.plugin.QueryableTypeDescriptor;
|
import sonia.scm.plugin.QueryableTypeDescriptor;
|
||||||
import sonia.scm.security.KeyGenerator;
|
import sonia.scm.security.KeyGenerator;
|
||||||
|
import sonia.scm.store.Condition;
|
||||||
import sonia.scm.store.IdHandlerForStores;
|
import sonia.scm.store.IdHandlerForStores;
|
||||||
import sonia.scm.store.QueryableMutableStore;
|
import sonia.scm.store.QueryableMutableStore;
|
||||||
import sonia.scm.store.StoreException;
|
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) {
|
private String marshal(T item) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(item);
|
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)));
|
conditions.add(new SQLCondition("=", new SQLField("ID"), new SQLValue(id)));
|
||||||
return conditions;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ package sonia.scm.store.sqlite;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import sonia.scm.plugin.QueryableTypeDescriptor;
|
import sonia.scm.plugin.QueryableTypeDescriptor;
|
||||||
import sonia.scm.store.Condition;
|
import sonia.scm.store.Condition;
|
||||||
@@ -51,6 +54,7 @@ import java.util.function.BooleanSupplier;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
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.computeColumnIdentifier;
|
||||||
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeTableName;
|
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeTableName;
|
||||||
|
|
||||||
@@ -82,7 +86,7 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Query<T, T> query(Condition<T>... conditions) {
|
public Query<T, T, ?> query(Condition<T>... conditions) {
|
||||||
return new SQLiteQuery<>(clazz, conditions);
|
return new SQLiteQuery<>(clazz, conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,14 +273,67 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
return connection;
|
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" – result type
|
||||||
|
* @param <SELF> "Self" – 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_RESULT> resultType;
|
||||||
private final Class<T> entityType;
|
private final Class<T> entityType;
|
||||||
private final Condition<T> condition;
|
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());
|
this(resultType, resultType, conjunct(conditions), Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +345,16 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
this.orderBy = orderBy;
|
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
|
@Override
|
||||||
public Optional<T_RESULT> findFirst() {
|
public Optional<T_RESULT> findFirst() {
|
||||||
return findAll(0, 1).stream().findFirst();
|
return findAll(0, 1).stream().findFirst();
|
||||||
@@ -314,17 +381,14 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void forEach(Consumer<T_RESULT> consumer, long offset, long limit) {
|
public void forEach(Consumer<T_RESULT> consumer, long offset, long limit) {
|
||||||
StringBuilder orderByBuilder = new StringBuilder();
|
String orderByString = getOrderByString();
|
||||||
if (orderBy != null && !orderBy.isEmpty()) {
|
|
||||||
toOrderBySQL(orderByBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
SQLSelectStatement sqlSelectQuery =
|
SQLSelectStatement sqlSelectQuery =
|
||||||
new SQLSelectStatement(
|
new SQLSelectStatement(
|
||||||
computeFields(),
|
computeFields(),
|
||||||
computeFromTable(),
|
computeFromTable(),
|
||||||
computeCondition(),
|
computeCondition(),
|
||||||
orderByBuilder.toString(),
|
orderByString,
|
||||||
limit,
|
limit,
|
||||||
offset
|
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
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@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);
|
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
|
@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);
|
List<OrderBy<T>> extendedOrderBy = new ArrayList<>(this.orderBy);
|
||||||
extendedOrderBy.add(new OrderBy<>(field, order));
|
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() {
|
private List<SQLField> computeFields() {
|
||||||
@@ -428,20 +502,19 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SQLNodeWithValue> computeCondition() {
|
List<SQLNodeWithValue> computeCondition() {
|
||||||
List<SQLNodeWithValue> conditions = new ArrayList<>();
|
List<SQLNodeWithValue> conditions = new ArrayList<>();
|
||||||
|
|
||||||
evaluateParentConditions(conditions);
|
evaluateParentConditions(conditions);
|
||||||
|
|
||||||
if (condition != null) {
|
if (condition != null) {
|
||||||
if (condition instanceof LeafCondition<T, ?> leafCondition) {
|
if (condition instanceof LeafCondition<T, ?> leafCondition) {
|
||||||
SQLCondition sqlCondition = SQLConditionMapper.mapToSQLCondition(leafCondition);
|
conditions.add(SQLConditionMapper.mapToSQLCondition(leafCondition));
|
||||||
conditions.add(sqlCondition);
|
|
||||||
}
|
}
|
||||||
if (condition instanceof LogicalCondition<T> logicalCondition) {
|
if (condition instanceof LogicalCondition<T> logicalCondition) {
|
||||||
SQLLogicalCondition sqlLogicalCondition = SQLConditionMapper.mapToSQLLogicalCondition(logicalCondition);
|
conditions.add(SQLConditionMapper.mapToSQLLogicalCondition(logicalCondition));
|
||||||
conditions.add(sqlLogicalCondition);
|
|
||||||
}
|
}
|
||||||
|
log.debug("Unsupported condition type: {}", condition.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return conditions;
|
return conditions;
|
||||||
@@ -467,16 +540,16 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
private T_RESULT extractResult(ResultSet resultSet) throws JsonProcessingException, SQLException {
|
private T_RESULT extractResult(ResultSet resultSet) throws JsonProcessingException, SQLException {
|
||||||
T entity = objectMapper.readValue(resultSet.getString(1), entityType);
|
T entity = objectMapper.readValue(resultSet.getString(1), entityType);
|
||||||
if (resultType.isAssignableFrom(Result.class)) {
|
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++) {
|
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);
|
String id = resultSet.getString(queryableTypeDescriptor.getTypes().length + 2);
|
||||||
return (T_RESULT) new Result<T>() {
|
return (T_RESULT) new Result<T>() {
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> getParentId(Class<?> clazz) {
|
public Optional<String> getParentId(Class<?> clazz) {
|
||||||
String parentClassName = computeColumnIdentifier(clazz.getName());
|
String parentClassName = computeColumnIdentifier(clazz.getName());
|
||||||
return Optional.ofNullable(parentIds.get(parentClassName));
|
return Optional.ofNullable(parentIdMapping.get(parentClassName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -494,38 +567,21 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T> Condition<T> conjunct(Condition<T>[] conditions) {
|
/* We explicitly suppress this warning since it's based on a generic whose information is lost during runtime,
|
||||||
if (conditions.length == 0) {
|
which can be conveniently circumvented with clone().
|
||||||
return null;
|
*/
|
||||||
} else if (conditions.length == 1) {
|
@SuppressWarnings("java:S2975")
|
||||||
return conditions[0];
|
@Override
|
||||||
} else {
|
public SQLiteQuery<T_RESULT, SELF> clone() {
|
||||||
return Conditions.and(conditions);
|
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 class TemporaryTableMaintenanceIterator implements MaintenanceIterator<T> {
|
||||||
private final PreparedStatement iterateStatement;
|
private final PreparedStatement iterateStatement;
|
||||||
private final List<SQLField> columns;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import org.junit.jupiter.api.BeforeEach;
|
|||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import sonia.scm.store.QueryableMutableStore;
|
||||||
|
import sonia.scm.store.QueryableStore;
|
||||||
import sonia.scm.user.User;
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -28,9 +30,11 @@ import java.sql.Connection;
|
|||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
@SuppressWarnings("resource")
|
@SuppressWarnings("resource")
|
||||||
class SQLiteQueryableMutableStoreTest {
|
class SQLiteQueryableMutableStoreTest {
|
||||||
@@ -309,4 +313,174 @@ class SQLiteQueryableMutableStoreTest {
|
|||||||
assertThat(store.getAll()).containsOnlyKeys("tricia");
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
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"})
|
@SuppressWarnings({"resource", "unchecked"})
|
||||||
class SQLiteQueryableStoreTest {
|
class SQLiteQueryableStoreTest {
|
||||||
@@ -1036,23 +1045,4 @@ class SQLiteQueryableStoreTest {
|
|||||||
enum Range {
|
enum Range {
|
||||||
SOLAR_SYSTEM, INNER_GALACTIC, INTER_GALACTIC
|
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");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import jakarta.xml.bind.annotation.XmlAccessType;
|
|||||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||||
import jakarta.xml.bind.annotation.XmlRootElement;
|
import jakarta.xml.bind.annotation.XmlRootElement;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import sonia.scm.store.QueryableStore;
|
||||||
import sonia.scm.store.QueryableType;
|
import sonia.scm.store.QueryableType;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -32,6 +33,26 @@ import java.util.Map;
|
|||||||
@QueryableType
|
@QueryableType
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
class Spaceship {
|
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;
|
String name;
|
||||||
SQLiteQueryableStoreTest.Range range;
|
SQLiteQueryableStoreTest.Range range;
|
||||||
Collection<String> crew;
|
Collection<String> crew;
|
||||||
|
|||||||
Reference in New Issue
Block a user