mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-10-26 00:56:09 +02:00
Add auto-increment ids for queryable types
Squash commits of branch feature/auto_increment: - Bootstrap auto-increment key option - Fix changelog - Document stuff - Fix unit test - Documentation - Use id generator in unit test extension - Do not use mockito for type descriptor in tests - Clean up - Fix indentation - Fix code formatting - Clean up
This commit is contained in:
@@ -441,6 +441,27 @@ public class MyEntity {
|
||||
}
|
||||
```
|
||||
|
||||
## Generated IDs with auto-increment
|
||||
|
||||
If you want to use auto-generated IDs, you can set the `idGenerator` property of the `@QueryableType` annotation to
|
||||
`IdGenerator.AUTO_INCREMENT`. This will cause the store to generate a numerical, incremented ID for each entity when it
|
||||
is stored (and no explicit ID is set). Note that this ID will be a `String` representation of the numerical value, so it
|
||||
can still be used as a `String` ID in the store. The ID will start at 1 and increment for each new entity stored.
|
||||
|
||||
```java
|
||||
import lombok.Data;
|
||||
import sonia.scm.store.QueryableType;
|
||||
import sonia.scm.store.Id;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
|
||||
@Data
|
||||
@QueryableType(idGenerator = IdGenerator.AUTO_INCREMENT)
|
||||
public class MyEntity {
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
This feature cannot be used in combination with an explicit ID field annotated with `@Id`.
|
||||
|
||||
## Update Steps
|
||||
|
||||
|
||||
2
gradle/changelog/auto_increment.yaml
Normal file
2
gradle/changelog/auto_increment.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Auto-increment ids for queryable types
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
public enum IdGenerator {
|
||||
/**
|
||||
* The default id generator using random strings with characters and numbers.
|
||||
*/
|
||||
DEFAULT,
|
||||
/**
|
||||
* An id generator that uses auto-incrementing ids.
|
||||
*/
|
||||
AUTO_INCREMENT,
|
||||
}
|
||||
@@ -53,4 +53,11 @@ public @interface QueryableType {
|
||||
* name of the queryable type.
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* The id generator to use for the queryable type. If no id generator is specified, the default id generator is used.
|
||||
* The default id generator generates the default random ids used everywhere else in SCM-Manager. If this is set
|
||||
* to {@link IdGenerator#AUTO_INCREMENT}, the store will use number based auto-incrementing ids.
|
||||
*/
|
||||
IdGenerator idGenerator() default IdGenerator.DEFAULT;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import jakarta.xml.bind.annotation.XmlAccessType;
|
||||
import jakarta.xml.bind.annotation.XmlAccessorType;
|
||||
import jakarta.xml.bind.annotation.XmlElement;
|
||||
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import lombok.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
import sonia.scm.xml.XmlArrayStringAdapter;
|
||||
|
||||
@ToString(callSuper = true)
|
||||
@@ -33,12 +37,19 @@ public class QueryableTypeDescriptor extends NamedClassElement {
|
||||
@XmlJavaTypeAdapter(XmlArrayStringAdapter.class)
|
||||
private String[] types;
|
||||
|
||||
QueryableTypeDescriptor(String name, String clazz, String[] types) {
|
||||
private IdGenerator idGenerator;
|
||||
|
||||
public QueryableTypeDescriptor(String name, String clazz, String[] types, IdGenerator idGenerator) {
|
||||
super(name, clazz);
|
||||
this.types = types;
|
||||
this.idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
public String[] getTypes() {
|
||||
return types == null ? new String[0] : types;
|
||||
}
|
||||
|
||||
public IdGenerator getIdGenerator() {
|
||||
return idGenerator == null ? IdGenerator.DEFAULT : idGenerator;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ import java.lang.annotation.Target;
|
||||
* Marks a field as id field. The value of this field will be used as id
|
||||
* in a {@link DataStore}. The field must be of type {@code String}. Only one
|
||||
* field in a class can be annotated with this annotation.
|
||||
* <br/>
|
||||
* Please note that this annotation is not compatible with stores using auto-incrementing
|
||||
* ids (stores for {@link QueryableType} with {@link IdGenerator#AUTO_INCREMENT}) option.
|
||||
* If you want to use auto-incrementing, do not annotate any field with this annotation.
|
||||
*/
|
||||
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.FIELD})
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
class IdFieldFinder {
|
||||
|
||||
private static final Map<Class<?>, Optional<Field>> FIELD_CACHE = new HashMap<>();
|
||||
|
||||
Optional<Field> getIdField(Class<?> clazz) {
|
||||
return FIELD_CACHE.computeIfAbsent(clazz, this::findIdField);
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3011") // we need reflection to access the field
|
||||
private Optional<Field> findIdField(Class<?> clazz) {
|
||||
for (var field : clazz.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(Id.class)) {
|
||||
if (field.getType() != String.class) {
|
||||
throw new StoreException(format("The field '%s' annotated with @Id in class %s must be of type String.", field.getName(), clazz.getName()));
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return Optional.of(field);
|
||||
}
|
||||
}
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return findIdField(clazz.getSuperclass());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -16,91 +16,8 @@
|
||||
|
||||
package sonia.scm.store;
|
||||
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
public interface IdHandlerForStores<T> {
|
||||
String put(T item);
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
public class IdHandlerForStores<T> {
|
||||
|
||||
private static final Map<Class<?>, Optional<Field>> FIELD_CACHE = new HashMap<>();
|
||||
|
||||
private final KeyGenerator keyGenerator;
|
||||
|
||||
private final Function<T, String> idExtractor;
|
||||
private final BiConsumer<T, String> idSetter;
|
||||
private final BiConsumer<String, T> doPut;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public IdHandlerForStores(Class<T> clazz, KeyGenerator keyGenerator, BiConsumer<String, T> doPut) {
|
||||
this.idExtractor = getIdExtractor(clazz);
|
||||
this.idSetter = getIdSetter(clazz);
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.doPut = doPut;
|
||||
}
|
||||
|
||||
private static <T> Function<T, String> getIdExtractor(Class<?> clazz) {
|
||||
return FIELD_CACHE.computeIfAbsent(clazz, IdHandlerForStores::getIdField)
|
||||
.map(field -> (Function<T, String>) item -> {
|
||||
try {
|
||||
return (String) field.get(item);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new StoreException("Failed to get id from object", e);
|
||||
}
|
||||
})
|
||||
.orElse(object -> null);
|
||||
}
|
||||
|
||||
private static <T> BiConsumer<T, String> getIdSetter(Class<?> clazz) {
|
||||
return FIELD_CACHE.computeIfAbsent(clazz, IdHandlerForStores::getIdField)
|
||||
.map(field -> (BiConsumer<T, String>) (object, id) -> {
|
||||
try {
|
||||
field.set(object, id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new StoreException("Failed to set id on object", e);
|
||||
}
|
||||
})
|
||||
.orElse((object, id) -> {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<Field> getIdField(Class<?> clazz) {
|
||||
for (var field : clazz.getDeclaredFields()) {
|
||||
if (field.isAnnotationPresent(Id.class)) {
|
||||
if (field.getType() != String.class) {
|
||||
throw new StoreException(format("The field '%s' annotated with @Id in class %s must be of type String.", field.getName(), clazz.getName()));
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return Optional.of(field);
|
||||
}
|
||||
}
|
||||
if (clazz.getSuperclass() != null) {
|
||||
return getIdField(clazz.getSuperclass());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public String put(T item) {
|
||||
String idFromItem = idExtractor.apply(item);
|
||||
if (idFromItem == null) {
|
||||
String id = keyGenerator.createKey();
|
||||
put(id, item);
|
||||
return id;
|
||||
} else {
|
||||
put(idFromItem, item);
|
||||
return idFromItem;
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String id, T item) {
|
||||
idSetter.accept(item, id);
|
||||
doPut.accept(id, item);
|
||||
}
|
||||
void put(String id, T item);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class IdHandlerForStoresForGeneratedId<T> implements IdHandlerForStores<T> {
|
||||
|
||||
private final KeyGenerator keyGenerator;
|
||||
|
||||
private final Function<T, String> idExtractor;
|
||||
private final BiConsumer<T, String> idSetter;
|
||||
private final BiConsumer<String, T> doPut;
|
||||
|
||||
public IdHandlerForStoresForGeneratedId(Class<T> clazz, KeyGenerator keyGenerator, BiConsumer<String, T> doPut) {
|
||||
this.idExtractor = getIdExtractor(clazz);
|
||||
this.idSetter = getIdSetter(clazz);
|
||||
this.keyGenerator = keyGenerator;
|
||||
this.doPut = doPut;
|
||||
}
|
||||
|
||||
private static <T> Function<T, String> getIdExtractor(Class<?> clazz) {
|
||||
return new IdFieldFinder().getIdField(clazz)
|
||||
|
||||
.map(field -> (Function<T, String>) item -> {
|
||||
try {
|
||||
return (String) field.get(item);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new StoreException("Failed to get id from object", e);
|
||||
}
|
||||
})
|
||||
.orElse(object -> null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3011") // we need reflection to access the field
|
||||
private static <T> BiConsumer<T, String> getIdSetter(Class<?> clazz) {
|
||||
return new IdFieldFinder().getIdField(clazz)
|
||||
.map(field -> (BiConsumer<T, String>) (object, id) -> {
|
||||
try {
|
||||
field.set(object, id);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new StoreException("Failed to set id on object", e);
|
||||
}
|
||||
})
|
||||
.orElse((object, id) -> {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
|
||||
public String put(T item) {
|
||||
String idFromItem = idExtractor.apply(item);
|
||||
if (idFromItem == null) {
|
||||
String id = keyGenerator.createKey();
|
||||
put(id, item);
|
||||
return id;
|
||||
} else {
|
||||
put(idFromItem, item);
|
||||
return idFromItem;
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String id, T item) {
|
||||
idSetter.accept(item, id);
|
||||
doPut.accept(id, item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class IdHandlerForStoresWithAutoIncrement<T> implements IdHandlerForStores<T> {
|
||||
|
||||
private final BiFunction<String, T, String> doPut;
|
||||
|
||||
public IdHandlerForStoresWithAutoIncrement(Class<T> clazz, BiFunction<String, T, String> doPut) {
|
||||
checkIdField(clazz);
|
||||
this.doPut = doPut;
|
||||
}
|
||||
|
||||
private static void checkIdField(Class<?> clazz) {
|
||||
new IdFieldFinder().getIdField(clazz)
|
||||
.ifPresent(x -> {
|
||||
throw new StoreException("The combination of @Id and auto-increment is not supported.");
|
||||
});
|
||||
}
|
||||
|
||||
public String put(T item) {
|
||||
return putWithAutoIncrement(null, item);
|
||||
}
|
||||
|
||||
public void put(String id, T item) {
|
||||
putWithAutoIncrement(id, item);
|
||||
}
|
||||
|
||||
private String putWithAutoIncrement(String id, T item) {
|
||||
return doPut.apply(id, item);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.CopyOnWrite;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.ConfigurationEntryStore;
|
||||
import sonia.scm.store.IdHandlerForStores;
|
||||
import sonia.scm.store.IdHandlerForStoresForGeneratedId;
|
||||
import sonia.scm.xml.XmlStreams;
|
||||
import sonia.scm.xml.XmlStreams.AutoCloseableXMLReader;
|
||||
import sonia.scm.xml.XmlStreams.AutoCloseableXMLWriter;
|
||||
@@ -52,7 +52,7 @@ class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
|
||||
private final TypedStoreContext<V> context;
|
||||
private final Map<String, V> entries = Maps.newHashMap();
|
||||
|
||||
private final IdHandlerForStores<V> idHandlerForStores;
|
||||
private final IdHandlerForStoresForGeneratedId<V> idHandlerForStores;
|
||||
|
||||
JAXBConfigurationEntryStore(File file, KeyGenerator keyGenerator, Class<V> type, TypedStoreContext<V> context) {
|
||||
this.file = file;
|
||||
@@ -64,7 +64,7 @@ class JAXBConfigurationEntryStore<V> implements ConfigurationEntryStore<V> {
|
||||
load();
|
||||
}
|
||||
}).withLockedFileForRead(file);
|
||||
this.idHandlerForStores = new IdHandlerForStores<>(type, keyGenerator, this::doPut);
|
||||
this.idHandlerForStores = new IdHandlerForStoresForGeneratedId<>(type, keyGenerator, this::doPut);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.CopyOnWrite;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.IdHandlerForStores;
|
||||
import sonia.scm.store.IdHandlerForStoresForGeneratedId;
|
||||
import sonia.scm.store.StoreException;
|
||||
import sonia.scm.xml.XmlStreams;
|
||||
|
||||
@@ -48,14 +48,14 @@ class JAXBDataStore<T> extends FileBasedStore<T> implements DataStore<T> {
|
||||
private final TypedStoreContext<T> context;
|
||||
private final DataFileCache.DataFileCacheInstance cache;
|
||||
|
||||
private final IdHandlerForStores<T> idHandlerForStores;
|
||||
private final IdHandlerForStoresForGeneratedId<T> idHandlerForStores;
|
||||
|
||||
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext<T> context, File directory, boolean readOnly, DataFileCache.DataFileCacheInstance cache) {
|
||||
super(directory, StoreConstants.FILE_EXTENSION, readOnly);
|
||||
this.cache = cache;
|
||||
this.directory = directory;
|
||||
this.context = context;
|
||||
this.idHandlerForStores = new IdHandlerForStores<>(context.getType(), keyGenerator, this::doPut);
|
||||
this.idHandlerForStores = new IdHandlerForStoresForGeneratedId<>(context.getType(), keyGenerator, this::doPut);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -60,6 +60,8 @@ class SQLSelectStatement extends ConditionalSQLStatement {
|
||||
|
||||
if (orderBy != null && !orderBy.isEmpty()) {
|
||||
query.append(" ORDER BY ").append(orderBy);
|
||||
} else {
|
||||
query.append(" ORDER BY ROWID");
|
||||
}
|
||||
|
||||
if (limit > 0) {
|
||||
|
||||
@@ -22,7 +22,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.plugin.QueryableTypeDescriptor;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.Condition;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
import sonia.scm.store.IdHandlerForStores;
|
||||
import sonia.scm.store.IdHandlerForStoresForGeneratedId;
|
||||
import sonia.scm.store.IdHandlerForStoresWithAutoIncrement;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.StoreException;
|
||||
|
||||
@@ -57,7 +60,10 @@ class SQLiteQueryableMutableStore<T> extends SQLiteQueryableStore<T> implements
|
||||
this.objectMapper = objectMapper;
|
||||
this.clazz = clazz;
|
||||
this.parentIds = parentIds;
|
||||
this.idHandlerForStores = new IdHandlerForStores<>(clazz, keyGenerator, this::doPut);
|
||||
this.idHandlerForStores =
|
||||
queryableTypeDescriptor.getIdGenerator() == IdGenerator.AUTO_INCREMENT ?
|
||||
new IdHandlerForStoresWithAutoIncrement<>(clazz, this::doPut) :
|
||||
new IdHandlerForStoresForGeneratedId<>(clazz, keyGenerator, this::doPut);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,7 +76,7 @@ class SQLiteQueryableMutableStore<T> extends SQLiteQueryableStore<T> implements
|
||||
idHandlerForStores.put(id, item);
|
||||
}
|
||||
|
||||
private void doPut(String id, T item) {
|
||||
private String doPut(String id, T item) {
|
||||
List<String> columnsToInsert = new ArrayList<>(Arrays.asList(parentIds));
|
||||
columnsToInsert.add(id);
|
||||
columnsToInsert.add(marshal(item));
|
||||
@@ -80,11 +86,18 @@ class SQLiteQueryableMutableStore<T> extends SQLiteQueryableStore<T> implements
|
||||
new SQLValue(columnsToInsert)
|
||||
);
|
||||
|
||||
executeWrite(
|
||||
return executeWrite(
|
||||
sqlInsertStatement,
|
||||
statement -> {
|
||||
statement.executeUpdate();
|
||||
return null;
|
||||
ResultSet generatedKeys = statement.getGeneratedKeys();
|
||||
if (generatedKeys.next()) {
|
||||
String generatedKey = generatedKeys.getString(1);
|
||||
log.trace("Generated key for item with id {}: {}", id, generatedKey);
|
||||
return generatedKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.sql.Statement.RETURN_GENERATED_KEYS;
|
||||
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeColumnIdentifier;
|
||||
import static sonia.scm.store.sqlite.SQLiteIdentifiers.computeTableName;
|
||||
|
||||
@@ -249,7 +250,7 @@ class SQLiteQueryableStore<T> implements QueryableStore<T>, QueryableMaintenance
|
||||
|
||||
private <R> R executeWithLock(SQLNodeWithValue sqlStatement, StatementCallback<R> callback, Lock writeLock, String sql) {
|
||||
writeLock.lock();
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql, RETURN_GENERATED_KEYS)) {
|
||||
sqlStatement.apply(statement, 1);
|
||||
return callback.apply(statement);
|
||||
} catch (SQLException | JsonProcessingException e) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package sonia.scm.store.sqlite;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.plugin.QueryableTypeDescriptor;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
import sonia.scm.store.StoreException;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -75,8 +76,17 @@ class TableCreator {
|
||||
for (String type : descriptor.getTypes()) {
|
||||
builder.append(computeColumnIdentifier(type)).append(" TEXT NOT NULL, ");
|
||||
}
|
||||
builder.append("ID TEXT NOT NULL, payload JSONB");
|
||||
builder.append(", PRIMARY KEY (");
|
||||
if (descriptor.getIdGenerator() == IdGenerator.AUTO_INCREMENT) {
|
||||
builder.append("ID INTEGER PRIMARY KEY, ");
|
||||
} else {
|
||||
builder.append("ID TEXT NOT NULL, ");
|
||||
}
|
||||
builder.append("payload JSONB");
|
||||
if (descriptor.getIdGenerator() == IdGenerator.AUTO_INCREMENT) {
|
||||
builder.append(", UNIQUE (");
|
||||
} else {
|
||||
builder.append(", PRIMARY KEY (");
|
||||
}
|
||||
for (String type : descriptor.getTypes()) {
|
||||
builder.append(computeColumnIdentifier(type)).append(", ");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package sonia.scm.store.sqlite;
|
||||
|
||||
import sonia.scm.plugin.QueryableTypeDescriptor;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
@@ -27,11 +28,15 @@ public class QueryableTypeDescriptorTestData {
|
||||
}
|
||||
|
||||
public static QueryableTypeDescriptor createDescriptor(String name, String clazz, String[] t) {
|
||||
return createDescriptor(name, clazz, t, IdGenerator.DEFAULT);
|
||||
}
|
||||
|
||||
public static QueryableTypeDescriptor createDescriptor(String name, String clazz, String[] t, IdGenerator idGenerator) {
|
||||
try {
|
||||
Constructor<QueryableTypeDescriptor> constructor = QueryableTypeDescriptor.class
|
||||
.getDeclaredConstructor(String.class, String.class, String[].class);
|
||||
.getDeclaredConstructor(String.class, String.class, String[].class, IdGenerator.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(name, clazz, t);
|
||||
return constructor.newInstance(name, clazz, t, idGenerator);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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.IdGenerator;
|
||||
import sonia.scm.store.QueryableMutableStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.user.User;
|
||||
@@ -81,6 +82,34 @@ class SQLiteQueryableMutableStoreTest {
|
||||
assertThat(resultSet.getString("name")).isEqualTo("McMillan");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPutObjectWithAutoIncrementId() {
|
||||
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString, IdGenerator.AUTO_INCREMENT).forClassWithIds(Spaceship.class);
|
||||
store.put(new Spaceship("42"));
|
||||
store.put(new Spaceship("23"));
|
||||
|
||||
Spaceship first = store.get("1");
|
||||
Spaceship second = store.get("2");
|
||||
|
||||
assertThat(first.getName()).isEqualTo("42");
|
||||
assertThat(second.getName()).isEqualTo("23");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPutObjectWithGivenIdsThoughAutoIncrementActivated() {
|
||||
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString, IdGenerator.AUTO_INCREMENT).forClassWithIds(Spaceship.class);
|
||||
store.put("42", new Spaceship("42", SQLiteQueryableStoreTest.Range.INTER_GALACTIC));
|
||||
store.put("23", new Spaceship("23", SQLiteQueryableStoreTest.Range.SOLAR_SYSTEM));
|
||||
|
||||
Spaceship first = store.get("42");
|
||||
Spaceship second = store.get("23");
|
||||
|
||||
assertThat(first.getName()).isEqualTo("42");
|
||||
assertThat(first.getRange()).isEqualTo(SQLiteQueryableStoreTest.Range.INTER_GALACTIC);
|
||||
assertThat(second.getName()).isEqualTo("23");
|
||||
assertThat(second.getRange()).isEqualTo(SQLiteQueryableStoreTest.Range.SOLAR_SYSTEM);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPutObjectWithSingleParent() throws SQLException {
|
||||
new StoreTestBuilder(connectionString, "sonia.Group").withIds("42")
|
||||
@@ -181,7 +210,7 @@ class SQLiteQueryableMutableStoreTest {
|
||||
|
||||
@Test
|
||||
void shouldGetObjectWithSingleParent() {
|
||||
new StoreTestBuilder(connectionString, new String[]{"sonia.Group"}).withIds("1337").put("tricia", new User("McMillan"));
|
||||
new StoreTestBuilder(connectionString, "sonia.Group").withIds("1337").put("tricia", new User("McMillan"));
|
||||
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group").withIds("42");
|
||||
store.put("tricia", new User("trillian"));
|
||||
|
||||
@@ -195,7 +224,7 @@ class SQLiteQueryableMutableStoreTest {
|
||||
|
||||
@Test
|
||||
void shouldGetObjectWithMultipleParents() {
|
||||
new StoreTestBuilder(connectionString, new String[]{"sonia.Company", "sonia.Group"}).withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
|
||||
new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
|
||||
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
|
||||
store.put("tricia", new User("trillian"));
|
||||
|
||||
@@ -209,7 +238,7 @@ class SQLiteQueryableMutableStoreTest {
|
||||
|
||||
@Test
|
||||
void shouldGetAllForSingleEntry() {
|
||||
new StoreTestBuilder(connectionString, new String[]{"sonia.Company", "sonia.Group"}).withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
|
||||
new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
|
||||
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
|
||||
store.put("tricia", new User("trillian"));
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ package sonia.scm.store.sqlite;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import sonia.scm.security.UUIDKeyGenerator;
|
||||
import sonia.scm.store.IdGenerator;
|
||||
import sonia.scm.store.QueryableMaintenanceStore;
|
||||
import sonia.scm.store.QueryableStore;
|
||||
import sonia.scm.user.User;
|
||||
@@ -45,9 +46,15 @@ class StoreTestBuilder {
|
||||
|
||||
private final String connectionString;
|
||||
private final String[] parentClasses;
|
||||
private final IdGenerator idGenerator;
|
||||
|
||||
StoreTestBuilder(String connectionString, String... parentClasses) {
|
||||
this(connectionString, IdGenerator.DEFAULT, parentClasses);
|
||||
}
|
||||
|
||||
StoreTestBuilder(String connectionString, IdGenerator idGenerator, String... parentClasses) {
|
||||
this.connectionString = connectionString;
|
||||
this.idGenerator = idGenerator;
|
||||
this.parentClasses = parentClasses;
|
||||
}
|
||||
|
||||
@@ -78,7 +85,7 @@ class StoreTestBuilder {
|
||||
connectionString,
|
||||
mapper,
|
||||
new UUIDKeyGenerator(),
|
||||
List.of(createDescriptor(clazz.getName(), parentClasses))
|
||||
List.of(createDescriptor("", clazz.getName(), parentClasses, idGenerator))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,6 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Loads {@link QueryableTypes} into a JUnit test suite.
|
||||
@@ -123,11 +120,13 @@ public class QueryableStoreExtension implements ParameterResolver, BeforeEachCal
|
||||
}
|
||||
|
||||
private QueryableTypeDescriptor createDescriptor(Class<?> clazz) {
|
||||
QueryableTypeDescriptor descriptor = mock(QueryableTypeDescriptor.class);
|
||||
QueryableType queryableAnnotation = clazz.getAnnotation(QueryableType.class);
|
||||
when(descriptor.getTypes()).thenReturn(stream(queryableAnnotation.value()).map(Class::getName).toArray(String[]::new));
|
||||
lenient().when(descriptor.getClazz()).thenReturn(clazz.getName());
|
||||
when(descriptor.getName()).thenReturn(queryableAnnotation.name());
|
||||
QueryableTypeDescriptor descriptor = new QueryableTypeDescriptor(
|
||||
queryableAnnotation.name(),
|
||||
clazz.getName(),
|
||||
stream(queryableAnnotation.value()).map(Class::getName).toArray(String[]::new),
|
||||
queryableAnnotation.idGenerator()
|
||||
);
|
||||
try {
|
||||
Class<?> storeFactoryClass = Class.forName(clazz.getName() + "StoreFactory");
|
||||
storeFactoryClasses.add(storeFactoryClass);
|
||||
|
||||
Reference in New Issue
Block a user