| 1 | package com.renomad.minum.database; | |
| 2 | ||
| 3 | import com.renomad.minum.logging.ILogger; | |
| 4 | import com.renomad.minum.state.Context; | |
| 5 | import com.renomad.minum.utils.FileUtils; | |
| 6 | ||
| 7 | import java.nio.file.Path; | |
| 8 | import java.util.*; | |
| 9 | import java.util.concurrent.Callable; | |
| 10 | import java.util.concurrent.ConcurrentHashMap; | |
| 11 | import java.util.concurrent.atomic.AtomicLong; | |
| 12 | import java.util.function.Function; | |
| 13 | ||
| 14 | /** | |
| 15 | * The abstract database class is a representation of the essential capabilities of | |
| 16 | * a Minum database. | |
| 17 | * <p> | |
| 18 | * There are two kinds of database provided, which only differ in how they | |
| 19 | * store data on disk. The "classic" kind, {@link Db}, stores each piece of | |
| 20 | * data in its own file. This is the simplest approach. | |
| 21 | * </p> | |
| 22 | * <p> | |
| 23 | * However, for significant speed gains, the new {@link DbEngine2} will | |
| 24 | * store each change as an append to a file, and will consolidate the on-disk | |
| 25 | * data occasionally, and on start. That way is thousands of times faster | |
| 26 | * to write to disk and to read from disk at startup. | |
| 27 | * </p> | |
| 28 | * @param <T> This is the type of data, which is always an implementation of | |
| 29 | * the {@link DbData} class. See the code of {@link com.renomad.minum.security.Inmate} | |
| 30 | * for an example of how this should look. | |
| 31 | */ | |
| 32 | public abstract class AbstractDb<T extends DbData<?>> { | |
| 33 | ||
| 34 | /** | |
| 35 | * The directory of the database on disk | |
| 36 | */ | |
| 37 | protected final Path dbDirectory; | |
| 38 | ||
| 39 | /** | |
| 40 | * An empty instance of the type of data stored by this | |
| 41 | * database, used for better handling of generics. | |
| 42 | */ | |
| 43 | protected final T emptyInstance; | |
| 44 | ||
| 45 | /** | |
| 46 | * Used for handling some file utilities in the database like creating directories | |
| 47 | */ | |
| 48 | protected final FileUtils fileUtils; | |
| 49 | ||
| 50 | /** | |
| 51 | * Holds some system-wide information that is beneficial for components of the database | |
| 52 | */ | |
| 53 | protected final Context context; | |
| 54 | ||
| 55 | /** | |
| 56 | * Used for providing logging throughout the database | |
| 57 | */ | |
| 58 | protected final ILogger logger; | |
| 59 | ||
| 60 | /** | |
| 61 | * The internal data structure of the database that resides in memory. The beating heart | |
| 62 | * of the database while it runs. | |
| 63 | */ | |
| 64 | protected final Map<Long, T> data; | |
| 65 | ||
| 66 | /** | |
| 67 | * The current index, used when creating new data items. Each item has its own | |
| 68 | * index value, this is where it is tracked. | |
| 69 | */ | |
| 70 | protected AtomicLong index; | |
| 71 | ||
| 72 | // components for registered indexes (for faster read performance) | |
| 73 | ||
| 74 | /** | |
| 75 | * This data structure is a nested map used for providing indexed data search. | |
| 76 | * <br> | |
| 77 | * The outer map is between the name of the index and the inner map. | |
| 78 | * <br> | |
| 79 | * The inner map is between strings and sets of items related to that string. | |
| 80 | */ | |
| 81 | protected final Map<String, Map<String, Set<T>>> registeredIndexes; | |
| 82 | ||
| 83 | /** | |
| 84 | * This map holds the functions that are registered to indexes, which are used | |
| 85 | * to construct the mappings between string values and items in the database. | |
| 86 | */ | |
| 87 | protected final Map<String, Function<T, String>> partitioningMap; | |
| 88 | ||
| 89 | protected AbstractDb(Path dbDirectory, Context context, T instance) { | |
| 90 | this.dbDirectory = dbDirectory; | |
| 91 | this.context = context; | |
| 92 | this.emptyInstance = instance; | |
| 93 | this.data = new ConcurrentHashMap<>(); | |
| 94 | this.logger = context.getLogger(); | |
| 95 | this.registeredIndexes = new HashMap<>(); | |
| 96 | this.partitioningMap = new HashMap<>(); | |
| 97 | this.fileUtils = new FileUtils(logger, context.getConstants()); | |
| 98 | } | |
| 99 | ||
| 100 | /** | |
| 101 | * Used to cleanly stop the database. | |
| 102 | * <br> | |
| 103 | * In the case of {@link Db} this will interrupt its internal queue and tell it | |
| 104 | * to finish up processing. | |
| 105 | * <br> | |
| 106 | * In the case of {@link DbEngine2} this will flush data to disk. | |
| 107 | */ | |
| 108 | public abstract void stop(); | |
| 109 | ||
| 110 | /** | |
| 111 | * Used to cleanly stop the database, with extra allowance of time | |
| 112 | * for cleanup. | |
| 113 | * <br> | |
| 114 | * Note that this method mostly applies to {@link Db}, and not as much | |
| 115 | * to {@link DbEngine2}. Only Db uses a processing queue on a thread which | |
| 116 | * is what requires a longer shutdown time for interruption. | |
| 117 | * @param count number of loops before we are done waiting for a clean close | |
| 118 | * and instead crash the instance closed. | |
| 119 | * @param sleepTime how long to wait, in milliseconds, for each iteration of the waiting loop. | |
| 120 | */ | |
| 121 | public abstract void stop(int count, int sleepTime); | |
| 122 | ||
| 123 | ||
| 124 | /** | |
| 125 | * Write data to the database. Use an index of 0 to store new data, and a positive | |
| 126 | * non-zero value to update data. | |
| 127 | * <p><em> | |
| 128 | * Example of adding new data to the database: | |
| 129 | * </em></p> | |
| 130 | * {@snippet : | |
| 131 | * final var newSalt = StringUtils.generateSecureRandomString(10); | |
| 132 | * final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt); | |
| 133 | * final var newUser = new User(0L, newUsername, hashedPassword, newSalt); | |
| 134 | * userDb.write(newUser); | |
| 135 | * } | |
| 136 | * <p><em> | |
| 137 | * Example of updating data: | |
| 138 | * </em></p> | |
| 139 | * {@snippet : | |
| 140 | * // write the updated salted password to the database | |
| 141 | * final var updatedUser = new User( | |
| 142 | * user().getIndex(), | |
| 143 | * user().getUsername(), | |
| 144 | * hashedPassword, | |
| 145 | * newSalt); | |
| 146 | * userDb.write(updatedUser); | |
| 147 | * } | |
| 148 | * | |
| 149 | * @param newData the data we are writing | |
| 150 | * @return the data with its new index assigned. | |
| 151 | */ | |
| 152 | public abstract T write(T newData); | |
| 153 | ||
| 154 | /** | |
| 155 | * Write database data into memory | |
| 156 | * @param newData the new data may be totally new or an update | |
| 157 | * @param newElementCreated if true, this is a create. If false, an update. | |
| 158 | */ | |
| 159 | protected void writeToMemory(T newData, boolean newElementCreated) { | |
| 160 | // if we got here, we are safe to proceed with putting the data into memory and disk | |
| 161 | logger.logTrace(() -> String.format("in thread %s, writing data %s", Thread.currentThread().getName(), newData)); | |
| 162 | T oldData = data.put(newData.getIndex(), newData); | |
| 163 | ||
| 164 | // handle the indexes differently depending on whether this is a create or delete | |
| 165 |
1
1. writeToMemory : negated conditional → KILLED |
if (newElementCreated) { |
| 166 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::addToIndexes → KILLED |
addToIndexes(newData); |
| 167 | } else { | |
| 168 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::removeFromIndexes → KILLED |
removeFromIndexes(oldData); |
| 169 |
1
1. writeToMemory : removed call to com/renomad/minum/database/AbstractDb::addToIndexes → KILLED |
addToIndexes(newData); |
| 170 | } | |
| 171 | } | |
| 172 | ||
| 173 | /** | |
| 174 | * When new data comes in, we look at its "index" value. If | |
| 175 | * it is zero, it's a create, and we assign it a new value. If it is | |
| 176 | * positive, it is an update, and we had better find it in the database | |
| 177 | * already, or else throw an exception. | |
| 178 | * @return true if a create, false if an update | |
| 179 | */ | |
| 180 | protected boolean processDataIndex(T newData) { | |
| 181 | // *** deal with the in-memory portion *** | |
| 182 | boolean newElementCreated = false; | |
| 183 | // create a new index for the data, if needed | |
| 184 |
1
1. processDataIndex : negated conditional → KILLED |
if (newData.getIndex() == 0) { |
| 185 |
1
1. processDataIndex : removed call to com/renomad/minum/database/DbData::setIndex → KILLED |
newData.setIndex(index.getAndIncrement()); |
| 186 | newElementCreated = true; | |
| 187 | } else { | |
| 188 | // if the data does not exist, and a positive non-zero | |
| 189 | // index was provided, throw an exception. | |
| 190 |
2
1. lambda$processDataIndex$1 : replaced boolean return with true for com/renomad/minum/database/AbstractDb::lambda$processDataIndex$1 → KILLED 2. lambda$processDataIndex$1 : negated conditional → KILLED |
boolean dataEntryExists = data.values().stream().anyMatch(x -> x.getIndex() == newData.getIndex()); |
| 191 |
1
1. processDataIndex : negated conditional → KILLED |
if (!dataEntryExists) { |
| 192 | throw new DbException( | |
| 193 | String.format("Positive indexes are only allowed when updating existing data. Index: %d", | |
| 194 | newData.getIndex())); | |
| 195 | } | |
| 196 | } | |
| 197 |
2
1. processDataIndex : replaced boolean return with false for com/renomad/minum/database/AbstractDb::processDataIndex → KILLED 2. processDataIndex : replaced boolean return with true for com/renomad/minum/database/AbstractDb::processDataIndex → KILLED |
return newElementCreated; |
| 198 | } | |
| 199 | ||
| 200 | /** | |
| 201 | * Delete data | |
| 202 | * <p><em>Example:</em></p> | |
| 203 | * {@snippet : | |
| 204 | * userDb.delete(user); | |
| 205 | * } | |
| 206 | * | |
| 207 | * @param dataToDelete the data we are serializing and writing | |
| 208 | */ | |
| 209 | public abstract void delete(T dataToDelete); | |
| 210 | ||
| 211 | ||
| 212 | /** | |
| 213 | * Remove a particular item from the internal data structure in memory | |
| 214 | */ | |
| 215 | protected void deleteFromMemory(T dataToDelete) { | |
| 216 | long dataIndex; | |
| 217 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (dataToDelete == null) { |
| 218 | throw new DbException("Invalid to be given a null value to delete"); | |
| 219 | } | |
| 220 | dataIndex = dataToDelete.getIndex(); | |
| 221 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (!data.containsKey(dataIndex)) { |
| 222 | throw new DbException("no data was found with index of " + dataIndex); | |
| 223 | } | |
| 224 | long finalDataIndex = dataIndex; | |
| 225 | logger.logTrace(() -> String.format("in thread %s, deleting data with index %d", Thread.currentThread().getName(), finalDataIndex)); | |
| 226 | data.remove(dataIndex); | |
| 227 |
1
1. deleteFromMemory : removed call to com/renomad/minum/database/AbstractDb::removeFromIndexes → KILLED |
removeFromIndexes(dataToDelete); |
| 228 | // if all the data was just now deleted, we need to | |
| 229 | // reset the index back to 1 | |
| 230 | ||
| 231 |
1
1. deleteFromMemory : negated conditional → KILLED |
if (data.isEmpty()) { |
| 232 |
1
1. deleteFromMemory : removed call to java/util/concurrent/atomic/AtomicLong::set → TIMED_OUT |
index.set(1); |
| 233 | } | |
| 234 | } | |
| 235 | ||
| 236 | ||
| 237 | /** | |
| 238 | * add the data to registered indexes. | |
| 239 | * <br> | |
| 240 | * For each of the registered indexes, | |
| 241 | * get the stored function to obtain a string value which helps divide | |
| 242 | * the overall data into partitions. | |
| 243 | */ | |
| 244 | protected void addToIndexes(T dbData) { | |
| 245 | ||
| 246 | for (var entry : partitioningMap.entrySet()) { | |
| 247 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
| 248 | // value to help partition / index the data | |
| 249 | Function<T, String> indexStringFunction = entry.getValue(); | |
| 250 | String propertyAsString = indexStringFunction.apply(dbData); | |
| 251 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
| 252 | synchronized (this) { | |
| 253 |
1
1. lambda$addToIndexes$3 : replaced return value with Collections.emptySet for com/renomad/minum/database/AbstractDb::lambda$addToIndexes$3 → KILLED |
stringIndexMap.computeIfAbsent(propertyAsString, k -> new HashSet<>()); |
| 254 | } | |
| 255 | // if the index-key provides a 1-to-1 mapping to items, like UUIDs, then | |
| 256 | // each value will have only one item in the collection. In other cases, | |
| 257 | // like when partitioning the data into multiple groups, there could easily | |
| 258 | // be many items per index value. | |
| 259 | Set<T> dataSet = stringIndexMap.get(propertyAsString); | |
| 260 | dataSet.add(dbData); | |
| 261 | } | |
| 262 | } | |
| 263 | ||
| 264 | /** | |
| 265 | * Run when an item is deleted from the database | |
| 266 | */ | |
| 267 | private void removeFromIndexes(T dbData) { | |
| 268 | for (var entry : partitioningMap.entrySet()) { | |
| 269 | // a function provided by the user to obtain an index-key: a unique or semi-unique | |
| 270 | // value to help partition / index the data | |
| 271 | Function<T, String> indexStringFunction = entry.getValue(); | |
| 272 | String propertyAsString = indexStringFunction.apply(dbData); | |
| 273 | Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey()); | |
| 274 | synchronized (this) { | |
| 275 |
2
1. lambda$removeFromIndexes$4 : replaced boolean return with true for com/renomad/minum/database/AbstractDb::lambda$removeFromIndexes$4 → KILLED 2. lambda$removeFromIndexes$4 : negated conditional → KILLED |
stringIndexMap.get(propertyAsString).removeIf(x -> x.getIndex() == dbData.getIndex()); |
| 276 | ||
| 277 | // in certain cases, we're removing one of the items that is indexed but | |
| 278 | // there are more left. If there's nothing left though, we'll remove the mapping. | |
| 279 |
1
1. removeFromIndexes : negated conditional → KILLED |
if (stringIndexMap.get(propertyAsString).isEmpty()) { |
| 280 | stringIndexMap.remove(propertyAsString); | |
| 281 | } | |
| 282 | } | |
| 283 | } | |
| 284 | } | |
| 285 | ||
| 286 | ||
| 287 | /** | |
| 288 | * Cause the database to immediately load all its data. | |
| 289 | * <p> | |
| 290 | * If this method is not invoked by the developer, then the data will be loaded | |
| 291 | * lazily, at the first point where it is needed, such as when | |
| 292 | * getting or writing data. | |
| 293 | * </p> | |
| 294 | * <p> | |
| 295 | * It is a good idea to regularly use this method at database | |
| 296 | * initialization. By doing so, the check for data corruption issues will | |
| 297 | * throw an exception that will cause the whole application to halt | |
| 298 | * immediately, which is far preferable to having the exception show | |
| 299 | * as a complaint in the logs. | |
| 300 | * </p> | |
| 301 | * <p> | |
| 302 | * An example database initialization with bells and whistles | |
| 303 | * is as follows: | |
| 304 | * </p> | |
| 305 | * <pre> | |
| 306 | * {@code | |
| 307 | * AbstractDb<PersonName> sampleDomainDb = context.getDb2("names", PersonName.EMPTY) | |
| 308 | * .registerIndex("name_index", name -> name) | |
| 309 | * .loadData(); | |
| 310 | * } | |
| 311 | * </pre> | |
| 312 | */ | |
| 313 | public abstract AbstractDb<T> loadData(); | |
| 314 | ||
| 315 | /** | |
| 316 | * This method returns a read-only view of the values of a database. | |
| 317 | * | |
| 318 | * <p><em>Example:</em></p> | |
| 319 | * {@snippet : | |
| 320 | * boolean doesUserAlreadyExist(String username) { | |
| 321 | * return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username)); | |
| 322 | * } | |
| 323 | * } | |
| 324 | */ | |
| 325 | public abstract Collection<T> values(); | |
| 326 | ||
| 327 | /** | |
| 328 | * Register an index in the database for higher performance data access. | |
| 329 | * <p> | |
| 330 | * This command should be run immediately after database declaration, | |
| 331 | * or more specifically, before any data is loaded from disk. Otherwise, | |
| 332 | * it would be possible to skip indexing that data. | |
| 333 | * </p> | |
| 334 | * <br> | |
| 335 | * Example: | |
| 336 | * <pre> | |
| 337 | * {@code | |
| 338 | * final var myDatabase = context.getDb("photos", Photograph.EMPTY) | |
| 339 | * .registerIndex("url", photo -> photo.getUrl()) | |
| 340 | * .loadData(); | |
| 341 | * } | |
| 342 | * </pre> | |
| 343 | * @param indexName a string used to distinguish this index. This string will be used again | |
| 344 | * when requesting data in a method like {@link #getIndexedData} or {@link #findExactlyOne} | |
| 345 | * @param keyObtainingFunction a function which obtains data from the data in this database, used | |
| 346 | * to partition the data into groups (potentially up to a 1-to-1 correspondence | |
| 347 | * between id and object) | |
| 348 | * @return the database instance if the registration succeeded | |
| 349 | * @throws DbException if the parameters are not entered properly, if the index has already | |
| 350 | * been registered, or if the data has already been loaded. It is necessary that | |
| 351 | * this is run immediately after declaring the database. To explain further: the data is not | |
| 352 | * actually loaded until the first time it is needed, such as running a write or delete, or | |
| 353 | * if the {@link #loadData()} ()} method is run. Creating an index map for the data that | |
| 354 | * is read from disk only occurs once, at data load time. Thus, it is crucial that the | |
| 355 | * registerIndex command is run before any data is loaded. | |
| 356 | */ | |
| 357 | public AbstractDb<T> registerIndex(String indexName, Function<T, String> keyObtainingFunction) { | |
| 358 |
1
1. registerIndex : negated conditional → KILLED |
if (keyObtainingFunction == null) { |
| 359 | throw new DbException("When registering an index, the partitioning algorithm must not be null"); | |
| 360 | } | |
| 361 |
2
1. registerIndex : negated conditional → KILLED 2. registerIndex : negated conditional → KILLED |
if (indexName == null || indexName.isBlank()) { |
| 362 | throw new DbException("When registering an index, value must be a non-empty string"); | |
| 363 | } | |
| 364 |
1
1. registerIndex : negated conditional → KILLED |
if (registeredIndexes.containsKey(indexName)) { |
| 365 | throw new DbException("It is forbidden to register the same index more than once. Duplicate index: \""+indexName+"\""); | |
| 366 | } | |
| 367 | HashMap<String, Set<T>> stringCollectionHashMap = new HashMap<>(); | |
| 368 | registeredIndexes.put(indexName, stringCollectionHashMap); | |
| 369 | partitioningMap.put(indexName, keyObtainingFunction); | |
| 370 |
1
1. registerIndex : replaced return value with null for com/renomad/minum/database/AbstractDb::registerIndex → KILLED |
return this; |
| 371 | } | |
| 372 | ||
| 373 | /** | |
| 374 | * Given the name of a registered index (see {@link #registerIndex(String, Function)}), | |
| 375 | * use the key to find the collection of data that matches it. | |
| 376 | * @param indexName the name of an index | |
| 377 | * @param key a string value that matches a partition calculated from the partition | |
| 378 | * function provided to {@link #registerIndex(String, Function)} | |
| 379 | * @return a collection of data, an empty collection if nothing found | |
| 380 | */ | |
| 381 | public Collection<T> getIndexedData(String indexName, String key) { | |
| 382 |
1
1. getIndexedData : negated conditional → KILLED |
if (!registeredIndexes.containsKey(indexName)) { |
| 383 | throw new DbException("There is no index registered on the database Db<"+this.emptyInstance.getClass().getSimpleName()+"> with a name of \""+indexName+"\""); | |
| 384 | } | |
| 385 | Set<T> values = registeredIndexes.get(indexName).get(key); | |
| 386 | // return an empty set rather than null | |
| 387 |
1
1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/AbstractDb::getIndexedData → KILLED |
return Objects.requireNonNullElseGet(values, Set::of); |
| 388 | } | |
| 389 | ||
| 390 | /** | |
| 391 | * Get a set of the currently-registered indexes on this database, useful | |
| 392 | * for debugging. | |
| 393 | */ | |
| 394 | public Set<String> getSetOfIndexes() { | |
| 395 |
1
1. getSetOfIndexes : replaced return value with Collections.emptySet for com/renomad/minum/database/AbstractDb::getSetOfIndexes → KILLED |
return partitioningMap.keySet(); |
| 396 | } | |
| 397 | ||
| 398 | /** | |
| 399 | * A utility to find exactly one item from the database. | |
| 400 | * <br> | |
| 401 | * This utility will search the indexes for a particular data by | |
| 402 | * indexName and indexKey. If not found, it will return null. If | |
| 403 | * found, it will be returned. If more than one are found, an exception | |
| 404 | * will be thrown. Use this tool when the data has been uniquely | |
| 405 | * indexed, like for example when setting a unique identifier into | |
| 406 | * each data. | |
| 407 | * @param indexName the name of the index, an arbitrary value set by the | |
| 408 | * user to help distinguish among potentially many indexes | |
| 409 | * set on this data | |
| 410 | * @param indexKey the key for this particular value, such as a UUID or a name | |
| 411 | * or any other way to partition the data | |
| 412 | * @see #findExactlyOne(String, String, Callable) | |
| 413 | */ | |
| 414 | public T findExactlyOne(String indexName, String indexKey) { | |
| 415 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → KILLED |
return findExactlyOne(indexName, indexKey, () -> null); |
| 416 | } | |
| 417 | ||
| 418 | /** | |
| 419 | * Find one item, with an alternate value if null | |
| 420 | * <br> | |
| 421 | * This utility will search the indexes for a particular data by | |
| 422 | * indexName and indexKey. If not found, it will return null. If | |
| 423 | * found, it will be returned. If more than one are found, an exception | |
| 424 | * will be thrown. Use this tool when the data has been uniquely | |
| 425 | * indexed, like for example when setting a unique identifier into | |
| 426 | * each data. | |
| 427 | * @param indexName the name of the index, an arbitrary value set by the | |
| 428 | * user to help distinguish among potentially many indexes | |
| 429 | * set on this data | |
| 430 | * @param indexKey the key for this particular value, such as a UUID or a name | |
| 431 | * or any other way to partition the data | |
| 432 | * @param alternate a functional interface that will be run if the result would | |
| 433 | * have been null, useful for situations where you don't want | |
| 434 | * the output to be null when nothing is found. | |
| 435 | * @see #findExactlyOne(String, String) | |
| 436 | */ | |
| 437 | public T findExactlyOne(String indexName, String indexKey, Callable<T> alternate) { | |
| 438 | Collection<T> indexedData = getIndexedData(indexName, indexKey); | |
| 439 |
1
1. findExactlyOne : negated conditional → KILLED |
if (indexedData.isEmpty()) { |
| 440 | try { | |
| 441 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → KILLED |
return alternate.call(); |
| 442 | } catch (Exception ex) { | |
| 443 | throw new DbException(ex); | |
| 444 | } | |
| 445 |
1
1. findExactlyOne : negated conditional → KILLED |
} else if (indexedData.size() == 1) { |
| 446 |
1
1. findExactlyOne : replaced return value with null for com/renomad/minum/database/AbstractDb::findExactlyOne → KILLED |
return indexedData.stream().findFirst().orElseThrow(); |
| 447 | } else { | |
| 448 | throw new DbException("More than one item found when searching database Db<%s> on index \"%s\" with key %s" | |
| 449 | .formatted(emptyInstance.getClass().getSimpleName(), indexName, indexKey)); | |
| 450 | } | |
| 451 | } | |
| 452 | } | |
Mutations | ||
| 165 |
1.1 |
|
| 166 |
1.1 |
|
| 168 |
1.1 |
|
| 169 |
1.1 |
|
| 184 |
1.1 |
|
| 185 |
1.1 |
|
| 190 |
1.1 2.2 |
|
| 191 |
1.1 |
|
| 197 |
1.1 2.2 |
|
| 217 |
1.1 |
|
| 221 |
1.1 |
|
| 227 |
1.1 |
|
| 231 |
1.1 |
|
| 232 |
1.1 |
|
| 253 |
1.1 |
|
| 275 |
1.1 2.2 |
|
| 279 |
1.1 |
|
| 358 |
1.1 |
|
| 361 |
1.1 2.2 |
|
| 364 |
1.1 |
|
| 370 |
1.1 |
|
| 382 |
1.1 |
|
| 387 |
1.1 |
|
| 395 |
1.1 |
|
| 415 |
1.1 |
|
| 439 |
1.1 |
|
| 441 |
1.1 |
|
| 445 |
1.1 |
|
| 446 |
1.1 |