Db.java

1
package com.renomad.minum.database;
2
3
import com.renomad.minum.state.Context;
4
import com.renomad.minum.logging.ILogger;
5
import com.renomad.minum.queue.AbstractActionQueue;
6
import com.renomad.minum.queue.ActionQueue;
7
import com.renomad.minum.utils.FileUtils;
8
9
import java.io.BufferedReader;
10
import java.io.FileReader;
11
import java.io.IOException;
12
import java.nio.charset.StandardCharsets;
13
import java.nio.file.Files;
14
import java.nio.file.Path;
15
import java.util.*;
16
import java.util.concurrent.Callable;
17
import java.util.concurrent.ConcurrentHashMap;
18
import java.util.concurrent.atomic.AtomicLong;
19
import java.util.concurrent.locks.Lock;
20
import java.util.concurrent.locks.ReentrantLock;
21
import java.util.function.Function;
22
23
import static com.renomad.minum.utils.Invariants.*;
24
25
/**
26
 * a memory-based disk-persisted database class.
27
 * @param <T> the type of data we'll be persisting (must extend from {@link DbData}
28
 */
29
public final class Db<T extends DbData<?>> {
30
31
    /**
32
     * The suffix we will apply to each database file
33
     */
34
    static final String DATABASE_FILE_SUFFIX = ".ddps";
35
    private final T emptyInstance;
36
37
    private final Lock loadDataLock = new ReentrantLock();
38
39
    /**
40
     * The full path to the file that contains the most-recent index
41
     * for this data.  As we add new files, each gets its own index
42
     * value.  When we start the program, we use this to determine
43
     * where to start counting for new indexes.
44
     */
45
    private final Path fullPathForIndexFile;
46
47
    final AtomicLong index;
48
49
    /**
50
     * An in-memory representation of the value of the current max index
51
     * that we store in index.ddps, in memory, so we can compare whether
52
     * we need to update the disk without checking the disk so often.
53
     */
54
    private long maxIndexOnDisk;
55
56
    private final Path dbDirectory;
57
    private final AbstractActionQueue actionQueue;
58
    private final ILogger logger;
59
    private final Map<Long, T> data;
60
    private final FileUtils fileUtils;
61
    private boolean hasLoadedData;
62
63
    // components for registered indexes (for faster read performance)
64
    private final Map<String, Map<String, Set<T>>> registeredIndexes;
65
    private final Map<String, Function<T, String>> partitioningMap;
66
67
    /**
68
     * Constructs an in-memory disk-persisted database.
69
     * Loading of data from disk happens at the first invocation of any command
70
     * changing or requesting data, such as {@link #write(DbData)}, {@link #delete(DbData)},
71
     * or {@link #values()}.  See the private method loadData() for details.
72
     * @param dbDirectory this uniquely names your database, and also sets the directory
73
     *                    name for this data.  The expected use case is to name this after
74
     *                    the data in question.  For example, "users", or "accounts".
75
     * @param context used to provide important state data to several components
76
     * @param instance an instance of the {@link DbData} object relevant for use in this database. Note
77
     *                 that each database (that is, each instance of this class), focuses on just one
78
     *                 data, which must be an implementation of {@link DbData}.
79
     */
80
    public Db(Path dbDirectory, Context context, T instance) {
81
        this.hasLoadedData = false;
82
        data = new ConcurrentHashMap<>();
83
        actionQueue = new ActionQueue("DatabaseWriter " + dbDirectory, context).initialize();
84
        this.logger = context.getLogger();
85
        this.dbDirectory = dbDirectory;
86
        this.fullPathForIndexFile = dbDirectory.resolve("index" + DATABASE_FILE_SUFFIX);
87
        this.emptyInstance = instance;
88
        this.fileUtils = new FileUtils(logger, context.getConstants());
89
        this.registeredIndexes = new HashMap<>();
90
        this.partitioningMap = new HashMap<>();
91
92 1 1. <init> : negated conditional → KILLED
        if (Files.exists(fullPathForIndexFile)) {
93
            long indexValue;
94
            try (var fileReader = new FileReader(fullPathForIndexFile.toFile(), StandardCharsets.UTF_8)) {
95
                try (BufferedReader br = new BufferedReader(fileReader)) {
96
                    String s = br.readLine();
97 1 1. <init> : negated conditional → KILLED
                    if (s == null) throw new DbException("index file for " + dbDirectory + " returned null when reading a line from it");
98
                    mustBeFalse(s.isBlank(), "Unless something is terribly broken, we expect a numeric value here");
99
                    String trim = s.trim();
100
                    indexValue = Long.parseLong(trim);
101
                }
102
            } catch (Exception e) {
103
                throw new DbException("Exception while reading "+fullPathForIndexFile+" in Db constructor", e);
104
            }
105
106
            this.index = new AtomicLong(indexValue);
107
108
        } else {
109
            this.index = new AtomicLong(1);
110
        }
111
112 2 1. <init> : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED
2. lambda$new$0 : removed call to com/renomad/minum/utils/FileUtils::makeDirectory → KILLED
        actionQueue.enqueue("create directory" + dbDirectory, () -> fileUtils.makeDirectory(dbDirectory));
113
    }
114
115
    /**
116
     * This function will stop the minum.database persistence cleanly.
117
     * <p>
118
     * In order to do this, we need to wait for our threads
119
     * to finish their work.  In particular, we
120
     * have offloaded our file writes to [actionQueue], which
121
     * has an internal thread for serializing all actions
122
     * on our minum.database
123
     * </p>
124
     */
125
    public void stop() {
126 1 1. stop : removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED
        actionQueue.stop();
127
    }
128
129
    /**
130
     * Similar to {@link #stop()} but gives more control over how long
131
     * we'll wait before crashing it closed.  See {@link ActionQueue#stop(int, int)}
132
     */
133
    public void stop(int count, int sleepTime) {
134 1 1. stop : removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED
        actionQueue.stop(count, sleepTime);
135
    }
136
137
    /**
138
     * Write data to the database.  Use an index of 0 to store new data, and a positive
139
     * non-zero value to update data.
140
     * <p><em>
141
     *     Example of adding new data to the database:
142
     * </p></em>
143
     * {@snippet :
144
     *          final var newSalt = StringUtils.generateSecureRandomString(10);
145
     *          final var hashedPassword = CryptoUtils.createPasswordHash(newPassword, newSalt);
146
     *          final var newUser = new User(0L, newUsername, hashedPassword, newSalt);
147
     *          userDb.write(newUser);
148
     * }
149
     * <p><em>
150
     *     Example of updating data:
151
     * </p></em>
152
     * {@snippet :
153
     *         // write the updated salted password to the database
154
     *         final var updatedUser = new User(
155
     *                 user().getIndex(),
156
     *                 user().getUsername(),
157
     *                 hashedPassword,
158
     *                 newSalt);
159
     *         userDb.write(updatedUser);
160
     * }
161
     *
162
     * @param newData the data we are writing
163
     */
164
    public T write(T newData) {
165 2 1. write : negated conditional → KILLED
2. write : changed conditional boundary → KILLED
        if (newData.getIndex() < 0) throw new DbException("Negative indexes are disallowed");
166
        // load data if needed
167 2 1. write : negated conditional → KILLED
2. write : removed call to com/renomad/minum/database/Db::loadData → KILLED
        if (!hasLoadedData) loadData();
168
169 1 1. write : removed call to com/renomad/minum/database/Db::writeToMemory → KILLED
        writeToMemory(newData);
170
171
        // *** now handle the disk portion ***
172 2 1. lambda$write$1 : removed call to com/renomad/minum/database/Db::writeToDisk → KILLED
2. write : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED
        actionQueue.enqueue("persist data to disk", () -> writeToDisk(newData));
173
174
        // returning the data at this point is the most convenient
175
        // way users will have access to the new index of the data.
176 1 1. write : replaced return value with null for com/renomad/minum/database/Db::write → KILLED
        return newData;
177
    }
178
179
    /**
180
     * Write database data into memory
181
     * @param newData the new data may be totally new or an update
182
     */
183
    private void writeToMemory(T newData) {
184
        // *** deal with the in-memory portion ***
185
        boolean newElementCreated = false;
186
        // create a new index for the data, if needed
187 1 1. writeToMemory : negated conditional → KILLED
        if (newData.getIndex() == 0) {
188 1 1. writeToMemory : removed call to com/renomad/minum/database/DbData::setIndex → KILLED
            newData.setIndex(index.getAndIncrement());
189
            newElementCreated = true;
190
        } else {
191
            // if the data does not exist, and a positive non-zero
192
            // index was provided, throw an exception.
193 2 1. lambda$writeToMemory$2 : negated conditional → KILLED
2. lambda$writeToMemory$2 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$writeToMemory$2 → KILLED
            boolean dataEntryExists = data.values().stream().anyMatch(x -> x.getIndex() == newData.getIndex());
194 1 1. writeToMemory : negated conditional → KILLED
            if (!dataEntryExists) {
195
                throw new DbException(
196
                        String.format("Positive indexes are only allowed when updating existing data. Index: %d",
197
                                newData.getIndex()));
198
            }
199
        }
200
        // if we got here, we are safe to proceed with putting the data into memory and disk
201
        logger.logTrace(() -> String.format("in thread %s, writing data %s", Thread.currentThread().getName(), newData));
202
        T oldData = data.put(newData.getIndex(), newData);
203
204
        // handle the indexes differently depending on whether this is a create or delete
205 1 1. writeToMemory : negated conditional → KILLED
        if (newElementCreated) {
206 1 1. writeToMemory : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED
            addToIndexes(newData);
207
        } else {
208 1 1. writeToMemory : removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED
            removeFromIndexes(oldData);
209 1 1. writeToMemory : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED
            addToIndexes(newData);
210
        }
211
    }
212
213
    private void writeToDisk(T newData) {
214
        final Path fullPath = dbDirectory.resolve(newData.getIndex() + DATABASE_FILE_SUFFIX);
215
        logger.logTrace(() -> String.format("writing data to %s", fullPath));
216
        String serializedData = newData.serialize();
217
        mustBeFalse(serializedData == null || serializedData.isBlank(),
218
                "the serialized form of data must not be blank. " +
219
                        "Is the serialization code written properly? Our datatype: " + emptyInstance);
220 1 1. writeToDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED
        fileUtils.writeString(fullPath, serializedData);
221 2 1. writeToDisk : changed conditional boundary → TIMED_OUT
2. writeToDisk : negated conditional → KILLED
        if (maxIndexOnDisk < index.get()) {
222
            maxIndexOnDisk = index.get();
223 1 1. writeToDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED
            fileUtils.writeString(fullPathForIndexFile, String.valueOf(maxIndexOnDisk));
224
        }
225
    }
226
227
    /**
228
     * Delete data
229
     * <p><em>Example:</p></em>
230
     * {@snippet :
231
     *      userDb.delete(user);
232
     * }
233
     * @param dataToDelete the data we are serializing and writing
234
     */
235
    public void delete(T dataToDelete) {
236
        // load data if needed
237 2 1. delete : negated conditional → KILLED
2. delete : removed call to com/renomad/minum/database/Db::loadData → KILLED
        if (!hasLoadedData) loadData();
238
239
        // deal with the in-memory portion
240 1 1. delete : removed call to com/renomad/minum/database/Db::deleteFromMemory → KILLED
        deleteFromMemory(dataToDelete);
241
242
        // now handle the disk portion
243 2 1. delete : removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED
2. lambda$delete$5 : removed call to com/renomad/minum/database/Db::deleteFromDisk → KILLED
        actionQueue.enqueue("delete data from disk", () -> deleteFromDisk(dataToDelete.getIndex()));
244
    }
245
246
    private void deleteFromDisk(long dataIndexToDelete) {
247
        final Path fullPath = dbDirectory.resolve(dataIndexToDelete + DATABASE_FILE_SUFFIX);
248
        logger.logTrace(() -> String.format("deleting data at %s", fullPath));
249
        try {
250 1 1. deleteFromDisk : negated conditional → KILLED
            if (!fullPath.toFile().exists()) {
251
                throw new DbException(fullPath + " must already exist before deletion");
252
            }
253 1 1. deleteFromDisk : removed call to java/nio/file/Files::delete → KILLED
            Files.delete(fullPath);
254 2 1. deleteFromDisk : changed conditional boundary → TIMED_OUT
2. deleteFromDisk : negated conditional → TIMED_OUT
            if (maxIndexOnDisk > index.get()) {
255
                maxIndexOnDisk = index.get();
256 1 1. deleteFromDisk : removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED
                fileUtils.writeString(fullPathForIndexFile, String.valueOf(maxIndexOnDisk));
257
258
            }
259
        } catch (Exception ex) {
260
            logger.logAsyncError(() -> "failed to delete file " + fullPath + " during deleteOnDisk. Exception: " + ex);
261
        }
262
    }
263
264
    private void deleteFromMemory(T dataToDelete) {
265
        long dataIndex;
266 1 1. deleteFromMemory : negated conditional → KILLED
        if (dataToDelete == null) {
267
            throw new DbException("Invalid to be given a null value to delete");
268
        }
269
        dataIndex = dataToDelete.getIndex();
270 1 1. deleteFromMemory : negated conditional → KILLED
        if (!data.containsKey(dataIndex)) {
271
            throw new DbException("no data was found with index of " + dataIndex);
272
        }
273
        long finalDataIndex = dataIndex;
274
        logger.logTrace(() -> String.format("in thread %s, deleting data with index %d", Thread.currentThread().getName(), finalDataIndex));
275
        data.remove(dataIndex);
276 1 1. deleteFromMemory : removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED
        removeFromIndexes(dataToDelete);
277
        // if all the data was just now deleted, we need to
278
        // reset the index back to 1
279
280 1 1. deleteFromMemory : negated conditional → KILLED
        if (data.isEmpty()) {
281 1 1. deleteFromMemory : removed call to java/util/concurrent/atomic/AtomicLong::set → TIMED_OUT
            index.set(1);
282
        }
283
    }
284
285
    /**
286
     * Grabs all the data from disk and returns it as a list.  This
287
     * method is run by various programs when the system first loads.
288
     */
289
    void loadDataFromDisk() {
290 1 1. loadDataFromDisk : negated conditional → KILLED
        if (! Files.exists(dbDirectory)) {
291
            logger.logDebug(() -> dbDirectory + " directory missing, adding nothing to the data list");
292
            return;
293
        }
294
295 1 1. loadDataFromDisk : removed call to com/renomad/minum/database/Db::walkAndLoad → KILLED
        walkAndLoad(dbDirectory);
296
    }
297
298
    void walkAndLoad(Path dbDirectory) {
299
        // walk through all the files in this directory, collecting
300
        // all regular files (non-subdirectories) except for index.ddps
301
        try (final var pathStream = Files.walk(dbDirectory)) {
302
            final var listOfFiles = pathStream.filter(path ->
303 2 1. lambda$walkAndLoad$10 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$walkAndLoad$10 → KILLED
2. lambda$walkAndLoad$10 : negated conditional → KILLED
                        Files.isRegularFile(path) &&
304 1 1. lambda$walkAndLoad$10 : negated conditional → KILLED
                        !path.getFileName().toString().startsWith("index")
305
            ).toList();
306
            for (Path p : listOfFiles) {
307 1 1. walkAndLoad : removed call to com/renomad/minum/database/Db::readAndDeserialize → KILLED
                readAndDeserialize(p);
308
            }
309
        } catch (IOException e) {
310
            throw new DbException(e);
311
        }
312
    }
313
314
    /**
315
     * Carry out the process of reading data files into our in-memory structure
316
     * @param p the path of a particular file
317
     */
318
    void readAndDeserialize(Path p) throws IOException {
319
        Path fileName = p.getFileName();
320 1 1. readAndDeserialize : negated conditional → KILLED
        if (fileName == null) throw new DbException("At readAndDeserialize, path " + p + " returned a null filename");
321
        String filename = fileName.toString();
322
        int startOfSuffixIndex = filename.indexOf('.');
323 1 1. readAndDeserialize : negated conditional → KILLED
        if(startOfSuffixIndex == -1) {
324
            throw new DbException("the files must have a ddps suffix, like 1.ddps.  filename: " + filename);
325
        }
326
        String fileContents = Files.readString(p);
327 1 1. readAndDeserialize : negated conditional → KILLED
        if (fileContents.isBlank()) {
328
            logger.logDebug( () -> fileName + " file exists but empty, skipping");
329
        } else {
330
            try {
331
                @SuppressWarnings("unchecked")
332
                T deserializedData = (T) emptyInstance.deserialize(fileContents);
333
                mustBeTrue(deserializedData != null, "deserialization of " + emptyInstance +
334
                        " resulted in a null value. Was the serialization method implemented properly?");
335
                int fileNameIdentifier = Integer.parseInt(filename.substring(0, startOfSuffixIndex));
336
                mustBeTrue(deserializedData.getIndex() == fileNameIdentifier,
337
                        "The filename must correspond to the data's index. e.g. 1.ddps must have an id of 1");
338
339
                // put the data into the in-memory data structure
340
                data.put(deserializedData.getIndex(), deserializedData);
341 1 1. readAndDeserialize : removed call to com/renomad/minum/database/Db::addToIndexes → KILLED
                addToIndexes(deserializedData);
342
343
            } catch (Exception e) {
344
                throw new DbException("Failed to deserialize "+ p +" with data (\""+fileContents+"\"). Caused by: " + e);
345
            }
346
        }
347
    }
348
349
    private void addToIndexes(T dbData) {
350
        // add the data to registered indexes.  For each of the registered indexes,
351
        // get the stored function to obtain a string value which helps divide
352
        // the overall data into partitions.
353
        for (var entry : partitioningMap.entrySet()) {
354
            // a function provided by the user to obtain an index-key: a unique or semi-unique
355
            // value to help partition / index the data
356
            Function<T, String> indexStringFunction = entry.getValue();
357
            String propertyAsString = indexStringFunction.apply(dbData);
358
            Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey());
359
            synchronized (this) {
360 1 1. lambda$addToIndexes$12 : replaced return value with Collections.emptySet for com/renomad/minum/database/Db::lambda$addToIndexes$12 → KILLED
                stringIndexMap.computeIfAbsent(propertyAsString, k -> new HashSet<>());
361
            }
362
            // if the index-key provides a 1-to-1 mapping to items, like UUIDs, then
363
            // each value will have only one item in the collection.  In other cases,
364
            // like when partitioning the data into multiple groups, there could easily
365
            // be many items per index value.
366
            Set<T> dataSet = stringIndexMap.get(propertyAsString);
367
            dataSet.add(dbData);
368
        }
369
    }
370
371
    /**
372
     * Run when an item is deleted from the database
373
     */
374
    private void removeFromIndexes(T dbData) {
375
        for (var entry : partitioningMap.entrySet()) {
376
            // a function provided by the user to obtain an index-key: a unique or semi-unique
377
            // value to help partition / index the data
378
            Function<T, String> indexStringFunction = entry.getValue();
379
            String propertyAsString = indexStringFunction.apply(dbData);
380
            Map<String, Set<T>> stringIndexMap = registeredIndexes.get(entry.getKey());
381
            synchronized (this) {
382 2 1. lambda$removeFromIndexes$13 : negated conditional → KILLED
2. lambda$removeFromIndexes$13 : replaced boolean return with true for com/renomad/minum/database/Db::lambda$removeFromIndexes$13 → KILLED
                stringIndexMap.get(propertyAsString).removeIf(x -> x.getIndex() == dbData.getIndex());
383
384
                // in certain cases, we're removing one of the items that is indexed but
385
                // there are more left.  If there's nothing left though, we'll remove the mapping.
386 1 1. removeFromIndexes : negated conditional → KILLED
                if (stringIndexMap.get(propertyAsString).isEmpty()) {
387
                    stringIndexMap.remove(propertyAsString);
388
                }
389
            }
390
        }
391
    }
392
393
    /**
394
     * This method provides read capability for the values of a database.
395
     * <br>
396
     * The returned collection is a read-only view over the data, through {@link Collections#unmodifiableCollection(Collection)}
397
     *
398
     * <p><em>Example:</em></p>
399
     * {@snippet :
400
     * boolean doesUserAlreadyExist(String username) {
401
     *     return userDb.values().stream().anyMatch(x -> x.getUsername().equals(username));
402
     * }
403
     * }
404
     */
405
    public Collection<T> values() {
406
        // load data if needed
407 2 1. values : removed call to com/renomad/minum/database/Db::loadData → TIMED_OUT
2. values : negated conditional → KILLED
        if (!hasLoadedData) loadData();
408
409 1 1. values : replaced return value with Collections.emptyList for com/renomad/minum/database/Db::values → KILLED
        return Collections.unmodifiableCollection(data.values());
410
    }
411
412
    /**
413
     * This is what loads the data from disk the
414
     * first time someone needs it.  Because it is
415
     * locked, only one thread can enter at
416
     * a time.  The first one in will load the data,
417
     * and the second will encounter a branch which skips loading.
418
     */
419
    private void loadData() {
420 1 1. loadData : removed call to java/util/concurrent/locks/Lock::lock → KILLED
        loadDataLock.lock(); // block threads here if multiple are trying to get in - only one gets in at a time
421
        try {
422 1 1. loadData : removed call to com/renomad/minum/database/Db::loadDataCore → KILLED
            loadDataCore(hasLoadedData, this::loadDataFromDisk);
423
            hasLoadedData = true;
424
        } finally {
425 1 1. loadData : removed call to java/util/concurrent/locks/Lock::unlock → KILLED
            loadDataLock.unlock();
426
        }
427
    }
428
429
    static void loadDataCore(boolean hasLoadedData, Runnable loadDataFromDisk) {
430 1 1. loadDataCore : negated conditional → KILLED
        if (!hasLoadedData) {
431 1 1. loadDataCore : removed call to java/lang/Runnable::run → KILLED
            loadDataFromDisk.run();
432
        }
433
    }
434
435
    /**
436
     * Register an index in the database for higher performance data access
437
     * @param indexName a string used to distinguish this index.  This string will be used again
438
     *                  when requesting data in a method like {@link #getIndexedData} or {@link #findExactlyOne}
439
     * @param keyObtainingFunction a function which obtains data from the data in this database, used
440
     *                             to partition the data into groups (potentially up to a 1-to-1 correspondence
441
     *                             between id and object)
442
     * @return true if the registration succeeded
443
     */
444
    public boolean registerIndex(String indexName, Function<T, String> keyObtainingFunction) {
445 1 1. registerIndex : negated conditional → KILLED
        if (keyObtainingFunction == null) {
446
            throw new DbException("When registering an index, the partitioning algorithm must not be null");
447
        }
448 2 1. registerIndex : negated conditional → KILLED
2. registerIndex : negated conditional → KILLED
        if (indexName == null || indexName.isBlank()) {
449
            throw new DbException("When registering an index, value must be a non-empty string");
450
        }
451 1 1. registerIndex : negated conditional → KILLED
        if (registeredIndexes.containsKey(indexName)) {
452
            throw new DbException("It is forbidden to register the same index more than once.  Duplicate index: \""+indexName+"\"");
453
        }
454
        HashMap<String, Set<T>> stringCollectionHashMap = new HashMap<>();
455
        registeredIndexes.put(indexName, stringCollectionHashMap);
456
        partitioningMap.put(indexName, keyObtainingFunction);
457 1 1. registerIndex : replaced boolean return with false for com/renomad/minum/database/Db::registerIndex → KILLED
        return true;
458
    }
459
460
    /**
461
     * Given the name of a registered index (see {@link #registerIndex(String, Function)}),
462
     * use the key to find the collection of data that matches it.
463
     * @param indexName the name of an index
464
     * @param key a string value that matches a partition calculated from the partition
465
     *            function provided to {@link #registerIndex(String, Function)}
466
     * @return a collection of data, an empty collection if nothing found
467
     */
468
    public Collection<T> getIndexedData(String indexName, String key) {
469 1 1. getIndexedData : negated conditional → KILLED
        if (!registeredIndexes.containsKey(indexName)) {
470
            throw new DbException("There is no index registered on the database Db<"+this.emptyInstance.getClass().getSimpleName()+"> with a name of \""+indexName+"\"");
471
        }
472
        Set<T> values = registeredIndexes.get(indexName).get(key);
473
        // return an empty set rather than null
474 1 1. getIndexedData : replaced return value with Collections.emptyList for com/renomad/minum/database/Db::getIndexedData → KILLED
        return Objects.requireNonNullElseGet(values, Set::of);
475
    }
476
477
    /**
478
     * Get a set of the currently-registered indexes on this database, useful
479
     * for debugging.
480
     */
481
    public Set<String> getSetOfIndexes() {
482 1 1. getSetOfIndexes : replaced return value with Collections.emptySet for com/renomad/minum/database/Db::getSetOfIndexes → KILLED
        return partitioningMap.keySet();
483
    }
484
485
    /**
486
     * A utility to find exactly one item from the database.
487
     * <br>
488
     * This utility will search the indexes for a particular data by
489
     * indexName and indexKey.  If not found, it will return null. If
490
     * found, it will be returned. If more than one are found, an exception
491
     * will be thrown.  Use this tool when the data has been uniquely
492
     * indexed, like for example when setting a unique identifier into
493
     * each data.
494
     * @param indexName the name of the index, an arbitrary value set by the
495
     *                  user to help distinguish among potentially many indexes
496
     *                  set on this data
497
     * @param indexKey the key for this particular value, such as a UUID or a name
498
     *                 or any other way to partition the data
499
     * @see #findExactlyOne(String, String, Callable)
500
     */
501
    public T findExactlyOne(String indexName, String indexKey) {
502 1 1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED
        return findExactlyOne(indexName, indexKey, () -> null);
503
    }
504
505
    /**
506
     * Find one item, with an alternate value if null
507
     * <br>
508
     * This utility will search the indexes for a particular data by
509
     * indexName and indexKey.  If not found, it will return null. If
510
     * found, it will be returned. If more than one are found, an exception
511
     * will be thrown.  Use this tool when the data has been uniquely
512
     * indexed, like for example when setting a unique identifier into
513
     * each data.
514
     * @param indexName the name of the index, an arbitrary value set by the
515
     *                  user to help distinguish among potentially many indexes
516
     *                  set on this data
517
     * @param indexKey the key for this particular value, such as a UUID or a name
518
     *                 or any other way to partition the data
519
     * @param alternate a functional interface that will be run if the result would
520
     *                  have been null, useful for situations where you don't want
521
     *                  the output to be null when nothing is found.
522
     * @see #findExactlyOne(String, String)
523
     */
524
    public T findExactlyOne(String indexName, String indexKey, Callable<T> alternate) {
525
        Collection<T> indexedData = getIndexedData(indexName, indexKey);
526 1 1. findExactlyOne : negated conditional → KILLED
        if (indexedData.isEmpty()) {
527
            try {
528 1 1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED
                return alternate.call();
529
            } catch (Exception ex) {
530
                throw new DbException(ex);
531
            }
532 1 1. findExactlyOne : negated conditional → KILLED
        } else if (indexedData.size() == 1) {
533 1 1. findExactlyOne : replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED
            return indexedData.stream().findFirst().orElseThrow();
534
        } else {
535
            throw new DbException("More than one item found when searching database Db<%s> on index \"%s\" with key %s"
536
                    .formatted(emptyInstance.getClass().getSimpleName(), indexName, indexKey));
537
        }
538
    }
539
}

Mutations

92

1.1
Location : <init>
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

97

1.1
Location : <init>
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
negated conditional → KILLED

112

1.1
Location : <init>
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED

2.2
Location : lambda$new$0
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/utils/FileUtils::makeDirectory → KILLED

126

1.1
Location : stop
Killed by : com.renomad.minum.database.DbTests.testStopping(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED

134

1.1
Location : stop
Killed by : com.renomad.minum.database.DbTests.testStopping2(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/queue/AbstractActionQueue::stop → KILLED

165

1.1
Location : write
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : write
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
changed conditional boundary → KILLED

167

1.1
Location : write
Killed by : com.renomad.minum.database.DbTests.test_Locking(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : write
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::loadData → KILLED

169

1.1
Location : write
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::writeToMemory → KILLED

172

1.1
Location : lambda$write$1
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::writeToDisk → KILLED

2.2
Location : write
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED

176

1.1
Location : write
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced return value with null for com/renomad/minum/database/Db::write → KILLED

187

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

188

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/DbData::setIndex → KILLED

193

1.1
Location : lambda$writeToMemory$2
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : lambda$writeToMemory$2
Killed by : com.renomad.minum.database.DbTests.test_Db_Write_and_Read(com.renomad.minum.database.DbTests)
replaced boolean return with true for com/renomad/minum/database/Db::lambda$writeToMemory$2 → KILLED

194

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
negated conditional → KILLED

205

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
negated conditional → KILLED

206

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::addToIndexes → KILLED

208

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED

209

1.1
Location : writeToMemory
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::addToIndexes → KILLED

220

1.1
Location : writeToDisk
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED

221

1.1
Location : writeToDisk
Killed by : com.renomad.minum.database.DbTests.testPoorlyNamedDbFile(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : writeToDisk
Killed by : none
changed conditional boundary → TIMED_OUT

223

1.1
Location : writeToDisk
Killed by : com.renomad.minum.database.DbTests.test_Locking_2(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED

237

1.1
Location : delete
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
negated conditional → KILLED

2.2
Location : delete
Killed by : com.renomad.minum.database.DbTests.test_Db_Write_and_Read(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::loadData → KILLED

240

1.1
Location : delete
Killed by : com.renomad.minum.database.DbTests.test_Db_Delete_EdgeCase_NullValue(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::deleteFromMemory → KILLED

243

1.1
Location : delete
Killed by : com.renomad.minum.database.DbTests.test_Locking(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/queue/AbstractActionQueue::enqueue → KILLED

2.2
Location : lambda$delete$5
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/database/Db::deleteFromDisk → KILLED

250

1.1
Location : deleteFromDisk
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
negated conditional → KILLED

253

1.1
Location : deleteFromDisk
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
removed call to java/nio/file/Files::delete → KILLED

254

1.1
Location : deleteFromDisk
Killed by : none
changed conditional boundary → TIMED_OUT

2.2
Location : deleteFromDisk
Killed by : none
negated conditional → TIMED_OUT

256

1.1
Location : deleteFromDisk
Killed by : com.renomad.minum.database.DbTests.test_Locking_2(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/utils/FileUtils::writeString → KILLED

266

1.1
Location : deleteFromMemory
Killed by : com.renomad.minum.database.DbTests.test_Db_Delete_EdgeCase_NullValue(com.renomad.minum.database.DbTests)
negated conditional → KILLED

270

1.1
Location : deleteFromMemory
Killed by : com.renomad.minum.database.DbTests.test_Db_Delete_EdgeCase_DoesNotExist(com.renomad.minum.database.DbTests)
negated conditional → KILLED

276

1.1
Location : deleteFromMemory
Killed by : com.renomad.minum.database.DbTests.testIndexesOnPartitionedData(com.renomad.minum.database.DbTests)
removed call to com/renomad/minum/database/Db::removeFromIndexes → KILLED

280

1.1
Location : deleteFromMemory
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
negated conditional → KILLED

281

1.1
Location : deleteFromMemory
Killed by : none
removed call to java/util/concurrent/atomic/AtomicLong::set → TIMED_OUT

290

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

295

1.1
Location : loadDataFromDisk
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/database/Db::walkAndLoad → KILLED

303

1.1
Location : lambda$walkAndLoad$10
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced boolean return with true for com/renomad/minum/database/Db::lambda$walkAndLoad$10 → KILLED

2.2
Location : lambda$walkAndLoad$10
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

304

1.1
Location : lambda$walkAndLoad$10
Killed by : com.renomad.minum.database.DbTests.test_Deserialization_EdgeCases(com.renomad.minum.database.DbTests)
negated conditional → KILLED

307

1.1
Location : walkAndLoad
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/database/Db::readAndDeserialize → KILLED

320

1.1
Location : readAndDeserialize
Killed by : com.renomad.minum.database.DbTests.testReadAndDeserialize_nullFilename(com.renomad.minum.database.DbTests)
negated conditional → KILLED

323

1.1
Location : readAndDeserialize
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
negated conditional → KILLED

327

1.1
Location : readAndDeserialize
Killed by : com.renomad.minum.database.DbTests.testDeserializerComplaints(com.renomad.minum.database.DbTests)
negated conditional → KILLED

341

1.1
Location : readAndDeserialize
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/database/Db::addToIndexes → KILLED

360

1.1
Location : lambda$addToIndexes$12
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced return value with Collections.emptySet for com/renomad/minum/database/Db::lambda$addToIndexes$12 → KILLED

382

1.1
Location : lambda$removeFromIndexes$13
Killed by : com.renomad.minum.database.DbTests.testIndex_Update(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : lambda$removeFromIndexes$13
Killed by : com.renomad.minum.database.DbTests.testIndexesOnPartitionedData(com.renomad.minum.database.DbTests)
replaced boolean return with true for com/renomad/minum/database/Db::lambda$removeFromIndexes$13 → KILLED

386

1.1
Location : removeFromIndexes
Killed by : com.renomad.minum.database.DbTests.testIndexesOnPartitionedData(com.renomad.minum.database.DbTests)
negated conditional → KILLED

407

1.1
Location : values
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
negated conditional → KILLED

2.2
Location : values
Killed by : none
removed call to com/renomad/minum/database/Db::loadData → TIMED_OUT

409

1.1
Location : values
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
replaced return value with Collections.emptyList for com/renomad/minum/database/Db::values → KILLED

420

1.1
Location : loadData
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
removed call to java/util/concurrent/locks/Lock::lock → KILLED

422

1.1
Location : loadData
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
removed call to com/renomad/minum/database/Db::loadDataCore → KILLED

425

1.1
Location : loadData
Killed by : com.renomad.minum.web.FullSystemTests.testFullSystem_EdgeCase_InstantlyClosed(com.renomad.minum.web.FullSystemTests)
removed call to java/util/concurrent/locks/Lock::unlock → KILLED

430

1.1
Location : loadDataCore
Killed by : com.renomad.minum.database.DbTests.testLoadDataCore_True(com.renomad.minum.database.DbTests)
negated conditional → KILLED

431

1.1
Location : loadDataCore
Killed by : com.renomad.minum.database.DbTests.testLoadDataCore_False(com.renomad.minum.database.DbTests)
removed call to java/lang/Runnable::run → KILLED

445

1.1
Location : registerIndex
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

448

1.1
Location : registerIndex
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

2.2
Location : registerIndex
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

451

1.1
Location : registerIndex
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_ExceptionThrownByPartitionAlgorithm(com.renomad.minum.database.DbTests)
negated conditional → KILLED

457

1.1
Location : registerIndex
Killed by : com.renomad.minum.database.DbTests.testIndex_EdgeCase_MultipleIndexes(com.renomad.minum.database.DbTests)
replaced boolean return with false for com/renomad/minum/database/Db::registerIndex → KILLED

469

1.1
Location : getIndexedData
Killed by : com.renomad.minum.database.DbTests.testIndex_NegativeCase_RequestingWithNoIndexRegistered(com.renomad.minum.database.DbTests)
negated conditional → KILLED

474

1.1
Location : getIndexedData
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced return value with Collections.emptyList for com/renomad/minum/database/Db::getIndexedData → KILLED

482

1.1
Location : getSetOfIndexes
Killed by : com.renomad.minum.database.DbTests.testIndex_GetListOfIndexes(com.renomad.minum.database.DbTests)
replaced return value with Collections.emptySet for com/renomad/minum/database/Db::getSetOfIndexes → KILLED

502

1.1
Location : findExactlyOne
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED

526

1.1
Location : findExactlyOne
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
negated conditional → KILLED

528

1.1
Location : findExactlyOne
Killed by : com.renomad.minum.database.DbTests.testSearchUtility(com.renomad.minum.database.DbTests)
replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED

532

1.1
Location : findExactlyOne
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
negated conditional → KILLED

533

1.1
Location : findExactlyOne
Killed by : com.renomad.minum.database.DbTests.testSearchUtils_ShouldAccomodateUsingIndexes(com.renomad.minum.database.DbTests)
replaced return value with null for com/renomad/minum/database/Db::findExactlyOne → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0