diff --git a/docs/en/development/storage.md b/docs/en/development/storage.md
index 7c112daaf4..de05733ecb 100644
--- a/docs/en/development/storage.md
+++ b/docs/en/development/storage.md
@@ -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
diff --git a/gradle/changelog/auto_increment.yaml b/gradle/changelog/auto_increment.yaml
new file mode 100644
index 0000000000..cd837c063f
--- /dev/null
+++ b/gradle/changelog/auto_increment.yaml
@@ -0,0 +1,2 @@
+- type: added
+ description: Auto-increment ids for queryable types
diff --git a/scm-annotations/src/main/java/sonia/scm/store/IdGenerator.java b/scm-annotations/src/main/java/sonia/scm/store/IdGenerator.java
new file mode 100644
index 0000000000..a55781dc69
--- /dev/null
+++ b/scm-annotations/src/main/java/sonia/scm/store/IdGenerator.java
@@ -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,
+}
diff --git a/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java b/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java
index baff25e490..8fe36fafd3 100644
--- a/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java
+++ b/scm-annotations/src/main/java/sonia/scm/store/QueryableType.java
@@ -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;
}
diff --git a/scm-core/src/main/java/sonia/scm/plugin/QueryableTypeDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/QueryableTypeDescriptor.java
index 72102ff249..17e2a62750 100644
--- a/scm-core/src/main/java/sonia/scm/plugin/QueryableTypeDescriptor.java
+++ b/scm-core/src/main/java/sonia/scm/plugin/QueryableTypeDescriptor.java
@@ -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;
+ }
}
diff --git a/scm-core/src/main/java/sonia/scm/store/Id.java b/scm-core/src/main/java/sonia/scm/store/Id.java
index 24cabe29c6..3d5176353f 100644
--- a/scm-core/src/main/java/sonia/scm/store/Id.java
+++ b/scm-core/src/main/java/sonia/scm/store/Id.java
@@ -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.
+ *
+ * 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})
diff --git a/scm-persistence/src/main/java/sonia/scm/store/IdFieldFinder.java b/scm-persistence/src/main/java/sonia/scm/store/IdFieldFinder.java
new file mode 100644
index 0000000000..feae991938
--- /dev/null
+++ b/scm-persistence/src/main/java/sonia/scm/store/IdFieldFinder.java
@@ -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, Optional> FIELD_CACHE = new HashMap<>();
+
+ Optional getIdField(Class> clazz) {
+ return FIELD_CACHE.computeIfAbsent(clazz, this::findIdField);
+ }
+
+ @SuppressWarnings("java:S3011") // we need reflection to access the field
+ private Optional 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();
+ }
+}
diff --git a/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStores.java b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStores.java
index 9ae69f2f0d..63512c29c9 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStores.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStores.java
@@ -16,91 +16,8 @@
package sonia.scm.store;
-import sonia.scm.security.KeyGenerator;
+public interface IdHandlerForStores {
+ 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 {
-
- private static final Map, Optional> FIELD_CACHE = new HashMap<>();
-
- private final KeyGenerator keyGenerator;
-
- private final Function idExtractor;
- private final BiConsumer idSetter;
- private final BiConsumer doPut;
-
- @SuppressWarnings("unchecked")
- public IdHandlerForStores(Class clazz, KeyGenerator keyGenerator, BiConsumer doPut) {
- this.idExtractor = getIdExtractor(clazz);
- this.idSetter = getIdSetter(clazz);
- this.keyGenerator = keyGenerator;
- this.doPut = doPut;
- }
-
- private static Function getIdExtractor(Class> clazz) {
- return FIELD_CACHE.computeIfAbsent(clazz, IdHandlerForStores::getIdField)
- .map(field -> (Function) 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 BiConsumer getIdSetter(Class> clazz) {
- return FIELD_CACHE.computeIfAbsent(clazz, IdHandlerForStores::getIdField)
- .map(field -> (BiConsumer) (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 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);
}
diff --git a/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresForGeneratedId.java b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresForGeneratedId.java
new file mode 100644
index 0000000000..6657525083
--- /dev/null
+++ b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresForGeneratedId.java
@@ -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 implements IdHandlerForStores {
+
+ private final KeyGenerator keyGenerator;
+
+ private final Function idExtractor;
+ private final BiConsumer idSetter;
+ private final BiConsumer doPut;
+
+ public IdHandlerForStoresForGeneratedId(Class clazz, KeyGenerator keyGenerator, BiConsumer doPut) {
+ this.idExtractor = getIdExtractor(clazz);
+ this.idSetter = getIdSetter(clazz);
+ this.keyGenerator = keyGenerator;
+ this.doPut = doPut;
+ }
+
+ private static Function getIdExtractor(Class> clazz) {
+ return new IdFieldFinder().getIdField(clazz)
+
+ .map(field -> (Function) 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 BiConsumer getIdSetter(Class> clazz) {
+ return new IdFieldFinder().getIdField(clazz)
+ .map(field -> (BiConsumer) (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);
+ }
+}
diff --git a/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresWithAutoIncrement.java b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresWithAutoIncrement.java
new file mode 100644
index 0000000000..709735678f
--- /dev/null
+++ b/scm-persistence/src/main/java/sonia/scm/store/IdHandlerForStoresWithAutoIncrement.java
@@ -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 implements IdHandlerForStores {
+
+ private final BiFunction doPut;
+
+ public IdHandlerForStoresWithAutoIncrement(Class clazz, BiFunction 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);
+ }
+}
diff --git a/scm-persistence/src/main/java/sonia/scm/store/file/JAXBConfigurationEntryStore.java b/scm-persistence/src/main/java/sonia/scm/store/file/JAXBConfigurationEntryStore.java
index 85ed9e20f4..9a246941b7 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/file/JAXBConfigurationEntryStore.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/file/JAXBConfigurationEntryStore.java
@@ -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 implements ConfigurationEntryStore {
private final TypedStoreContext context;
private final Map entries = Maps.newHashMap();
- private final IdHandlerForStores idHandlerForStores;
+ private final IdHandlerForStoresForGeneratedId idHandlerForStores;
JAXBConfigurationEntryStore(File file, KeyGenerator keyGenerator, Class type, TypedStoreContext context) {
this.file = file;
@@ -64,7 +64,7 @@ class JAXBConfigurationEntryStore implements ConfigurationEntryStore {
load();
}
}).withLockedFileForRead(file);
- this.idHandlerForStores = new IdHandlerForStores<>(type, keyGenerator, this::doPut);
+ this.idHandlerForStores = new IdHandlerForStoresForGeneratedId<>(type, keyGenerator, this::doPut);
}
@Override
diff --git a/scm-persistence/src/main/java/sonia/scm/store/file/JAXBDataStore.java b/scm-persistence/src/main/java/sonia/scm/store/file/JAXBDataStore.java
index c3dc91b930..0ef6b404da 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/file/JAXBDataStore.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/file/JAXBDataStore.java
@@ -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 extends FileBasedStore implements DataStore {
private final TypedStoreContext context;
private final DataFileCache.DataFileCacheInstance cache;
- private final IdHandlerForStores idHandlerForStores;
+ private final IdHandlerForStoresForGeneratedId idHandlerForStores;
JAXBDataStore(KeyGenerator keyGenerator, TypedStoreContext 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
diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLSelectStatement.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLSelectStatement.java
index c5093f138a..a0c95733aa 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLSelectStatement.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLSelectStatement.java
@@ -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) {
diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStore.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStore.java
index ffe59118d6..ed55e52548 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStore.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStore.java
@@ -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 extends SQLiteQueryableStore 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 extends SQLiteQueryableStore implements
idHandlerForStores.put(id, item);
}
- private void doPut(String id, T item) {
+ private String doPut(String id, T item) {
List columnsToInsert = new ArrayList<>(Arrays.asList(parentIds));
columnsToInsert.add(id);
columnsToInsert.add(marshal(item));
@@ -80,11 +86,18 @@ class SQLiteQueryableMutableStore extends SQLiteQueryableStore 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;
+ }
}
);
}
diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java
index b76c62a51a..e8c35eaf87 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/SQLiteQueryableStore.java
@@ -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 implements QueryableStore, QueryableMaintenance
private R executeWithLock(SQLNodeWithValue sqlStatement, StatementCallback 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) {
diff --git a/scm-persistence/src/main/java/sonia/scm/store/sqlite/TableCreator.java b/scm-persistence/src/main/java/sonia/scm/store/sqlite/TableCreator.java
index 80879755bd..f32d65ae1b 100644
--- a/scm-persistence/src/main/java/sonia/scm/store/sqlite/TableCreator.java
+++ b/scm-persistence/src/main/java/sonia/scm/store/sqlite/TableCreator.java
@@ -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(", ");
}
diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/QueryableTypeDescriptorTestData.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/QueryableTypeDescriptorTestData.java
index dcdf8d4abb..5d9e386530 100644
--- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/QueryableTypeDescriptorTestData.java
+++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/QueryableTypeDescriptorTestData.java
@@ -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 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);
}
diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStoreTest.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStoreTest.java
index aeaa2ec3ee..15f57e2c34 100644
--- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStoreTest.java
+++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/SQLiteQueryableMutableStoreTest.java
@@ -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 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 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 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 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 store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("tricia", new User("trillian"));
diff --git a/scm-persistence/src/test/java/sonia/scm/store/sqlite/StoreTestBuilder.java b/scm-persistence/src/test/java/sonia/scm/store/sqlite/StoreTestBuilder.java
index 2574344690..bdc4aa0846 100644
--- a/scm-persistence/src/test/java/sonia/scm/store/sqlite/StoreTestBuilder.java
+++ b/scm-persistence/src/test/java/sonia/scm/store/sqlite/StoreTestBuilder.java
@@ -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))
);
}
}
diff --git a/scm-queryable-test/src/main/java/sonia/scm/store/QueryableStoreExtension.java b/scm-queryable-test/src/main/java/sonia/scm/store/QueryableStoreExtension.java
index abd73dac1e..05634ffb3f 100644
--- a/scm-queryable-test/src/main/java/sonia/scm/store/QueryableStoreExtension.java
+++ b/scm-queryable-test/src/main/java/sonia/scm/store/QueryableStoreExtension.java
@@ -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);