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