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:
Rene Pfeuffer
2025-06-13 12:00:37 +02:00
parent 18821f5301
commit cc48945200
20 changed files with 351 additions and 114 deletions

View File

@@ -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

View File

@@ -0,0 +1,2 @@
- type: added
description: Auto-increment ids for queryable types

View File

@@ -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,
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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})

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;
}
}
);
}

View File

@@ -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) {

View File

@@ -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(", ");
}

View File

@@ -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);
}

View File

@@ -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"));

View File

@@ -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))
);
}
}

View File

@@ -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);