mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-10-26 08:06:09 +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
|
||||
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,
|
||||
|
||||
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 {
|
||||
void transactional(BooleanSupplier callback);
|
||||
|
||||
@Override
|
||||
MutableQuery<T, ?> query(Condition<T>... conditions);
|
||||
|
||||
@Override
|
||||
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.
|
||||
* @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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" – 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> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user