1 /**
2 Copyright: Copyright (c) 2018-2021, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 This module contains the a basic database interface that have minimal
11 dependencies on internal modules.  It is intended to be reusable from the test
12 suite.
13 
14 The only acceptable dependency are:
15  * ../type.d
16  * ..backend/type.d
17  * ../database/type.d
18  * ../database/schema.d
19 */
20 module dextool.plugin.mutate.backend.database.standalone;
21 
22 import core.time : Duration, dur;
23 import logger = std.experimental.logger;
24 import std.algorithm : copy, map, joiner, filter;
25 import std.array : Appender, appender, array, empty;
26 import std.conv : to;
27 import std.datetime : SysTime, Clock;
28 import std.exception : collectException;
29 import std.format : format;
30 import std.path : relativePath;
31 import std.regex : Regex, matchFirst;
32 import std.typecons : Nullable, Flag, No;
33 
34 import d2sqlite3 : SqlDatabase = Database;
35 import miniorm : Miniorm, select, insert, insertOrReplace, delete_,
36     insertOrIgnore, toSqliteDateTime, fromSqLiteDateTime, Bind;
37 import my.named_type;
38 import my.optional;
39 import my.term_color;
40 import my.set;
41 
42 import dextool.type : AbsolutePath, Path, ExitStatusType;
43 
44 import dextool.plugin.mutate.backend.database.schema;
45 import dextool.plugin.mutate.backend.database.type;
46 import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum, Language, Offset;
47 import dextool.plugin.mutate.type : MutationOrder;
48 
49 /** Database wrapper with minimal dependencies.
50  */
51 struct Database {
52     Miniorm db;
53     alias db this;
54 
55     /** Create a database by either opening an existing or initializing a new.
56      *
57      * Params:
58      *  db = path to the database
59      */
60     static auto make(string db) @safe {
61         return Database(initializeDB(db));
62     }
63 
64     bool isToolVersionDifferent(ToolVersion compareTo) @trusted {
65         foreach (a; db.run(select!DextoolVersionTable)) {
66             return a.checksum != compareTo.get;
67         }
68         // if there is no tool version recorded then assume it is different.
69         return true;
70     }
71 
72     /// Update the version of the tool.
73     void updateToolVersion(const ToolVersion tv) @trusted {
74         db.run(delete_!DextoolVersionTable);
75         db.run(insert!DextoolVersionTable, DextoolVersionTable(tv.get));
76     }
77 
78     /** Add all mutants with the specific status to the worklist.
79      */
80     void updateWorklist(const Mutation.Kind[] kinds, const Mutation.Status status,
81             const long basePrio = 100, const MutationOrder userOrder = MutationOrder.random) @trusted {
82         const order = fromOrder(userOrder);
83 
84         const sql = format!"INSERT OR IGNORE INTO %s (id,prio)
85             SELECT t1.id,%s FROM %s t0, %s t1 WHERE t0.kind IN (%(%s,%)) AND
86             t0.st_id = t1.id AND
87             t1.status = :status
88             "(mutantWorklistTable, order,
89                 mutationTable, mutationStatusTable, kinds.map!(a => cast(int) a));
90         auto stmt = db.prepare(sql);
91         stmt.get.bind(":status", cast(long) status);
92         stmt.get.bind(":base_prio", basePrio);
93         stmt.get.execute;
94     }
95 
96     /// Add a mutant to the worklist.
97     void addToWorklist(const MutationStatusId id, const long basePrio = 0,
98             const MutationOrder userOrder = MutationOrder.consecutive) @trusted {
99         const order = fromOrder(userOrder);
100         const sql = format!"INSERT OR REPLACE INTO %s (id,prio)
101             SELECT t1.id,%s FROM %s t1 WHERE t1.id = :id
102             "(mutantWorklistTable, order, mutationStatusTable);
103         auto stmt = db.prepare(sql);
104         stmt.get.bind(":id", id.get);
105         stmt.get.bind(":base_prio", basePrio);
106         stmt.get.execute;
107     }
108 
109     /// Remove a mutant from the worklist.
110     void removeFromWorklist(const MutationStatusId id) @trusted {
111         static immutable sql = format!"DELETE FROM %1$s WHERE id = :id"(mutantWorklistTable);
112         auto stmt = db.prepare(sql);
113         stmt.get.bind(":id", id.get);
114         stmt.get.execute;
115     }
116 
117     void clearWorklist() @trusted {
118         static immutable sql = format!"DELETE FROM %1$s"(mutantWorklistTable);
119         auto stmt = db.prepare(sql);
120         stmt.get.execute;
121     }
122 
123     long getWorklistCount() @trusted {
124         static immutable sql = format!"SELECT count(*) FROM %1$s"(mutantWorklistTable);
125         auto stmt = db.prepare(sql);
126         auto res = stmt.get.execute;
127         return res.oneValue!long;
128     }
129 
130     /// If the file has already been analyzed.
131     bool isAnalyzed(const Path p) @trusted {
132         auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1");
133         stmt.get.bind(":path", cast(string) p);
134         auto res = stmt.get.execute;
135         return res.oneValue!long != 0;
136     }
137 
138     /// If the file has already been analyzed.
139     bool isAnalyzed(const Path p, const Checksum cs) @trusted {
140         auto stmt = db.prepare(
141                 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1");
142         stmt.get.bind(":path", cast(string) p);
143         stmt.get.bind(":cs0", cast(long) cs.c0);
144         stmt.get.bind(":cs1", cast(long) cs.c1);
145         auto res = stmt.get.execute;
146         return res.oneValue!long != 0;
147     }
148 
149     bool exists(MutationStatusId id) {
150         static immutable s = format!"SELECT COUNT(*) FROM %s WHERE id=:id LIMIT 1"(
151                 mutationStatusTable);
152         auto stmt = db.prepare(s);
153         stmt.get.bind(":id", cast(long) id);
154         auto res = stmt.get.execute;
155         return res.oneValue!long == 0;
156     }
157 
158     bool isMarked(MutationId id) @trusted {
159         static immutable s = format!"SELECT COUNT(*) FROM %s WHERE st_id IN
160             (SELECT st_id FROM %s WHERE id=:id)"(
161                 markedMutantTable, mutationTable);
162         auto stmt = db.prepare(s);
163         stmt.get.bind(":id", cast(long) id);
164         auto res = stmt.get.execute;
165         return res.oneValue!long != 0;
166     }
167 
168     /// All marked mutants whom have a mutation status checksum that has been removed from the database.
169     MarkedMutant[] getLostMarkings() @trusted {
170         static immutable sql = format!"SELECT checksum0 FROM %s
171             WHERE
172             checksum0 NOT IN (SELECT checksum0 FROM %s)"(
173                 markedMutantTable, mutationStatusTable);
174 
175         auto stmt = db.prepare(sql);
176         auto app = appender!(MarkedMutant[])();
177         foreach (res; stmt.get.execute) {
178             foreach (m; db.run(select!MarkedMutantTbl.where("checksum0 = :cs0",
179                     Bind("cs0")), res.peek!long(0))) {
180                 app.put(.make(m));
181             }
182         }
183 
184         return app.data;
185     }
186 
187     Nullable!FileId getFileId(const Path p) @trusted {
188         static immutable sql = format("SELECT id FROM %s WHERE path=:path", filesTable);
189         auto stmt = db.prepare(sql);
190         stmt.get.bind(":path", p.toString);
191         auto res = stmt.get.execute;
192 
193         typeof(return) rval;
194         if (!res.empty)
195             rval = FileId(res.oneValue!long);
196         return rval;
197     }
198 
199     /// Returns: the path ID for the mutant.
200     Nullable!FileId getFileId(const MutationId id) @trusted {
201         static immutable sql = format("SELECT t1.file_id
202             FROM %s t0, %s t1
203             WHERE t0.id = :id AND t0.mp_id = t1.id",
204                 mutationTable, mutationPointTable);
205         auto stmt = db.prepare(sql);
206         stmt.get.bind(":id", cast(long) id);
207 
208         typeof(return) rval;
209         foreach (ref r; stmt.get.execute)
210             rval = FileId(r.peek!long(0));
211         return rval;
212     }
213 
214     /// Returns: the file path that the id correspond to.
215     Nullable!Path getFile(const FileId id) @trusted {
216         static immutable sql = format("SELECT path FROM %s WHERE id = :id", filesTable);
217         auto stmt = db.prepare(sql);
218         stmt.get.bind(":id", id.get);
219 
220         typeof(return) rval;
221         foreach (ref r; stmt.get.execute)
222             rval = Path(r.peek!string(0));
223         return rval;
224     }
225 
226     Optional!Language getFileIdLanguage(const FileId id) @trusted {
227         static immutable sql = format!"SELECT lang FROM %s WHERE id = :id"(filesTable);
228         auto stmt = db.prepare(sql);
229         stmt.get.bind(":id", id.get);
230 
231         foreach (ref r; stmt.get.execute)
232             return some(r.peek!ubyte(0).to!Language);
233         return none!Language;
234     }
235 
236     /// Returns: all files tagged as a root.
237     FileId[] getRootFiles() @trusted {
238         static immutable sql = format!"SELECT id FROM %s WHERE root=1"(filesTable);
239 
240         auto app = appender!(FileId[])();
241         auto stmt = db.prepare(sql);
242         foreach (ref r; stmt.get.execute) {
243             app.put(r.peek!long(0).FileId);
244         }
245         return app.data;
246     }
247 
248     /// Remove the file with all mutations that are coupled to it.
249     void removeFile(const Path p) @trusted {
250         auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable));
251         stmt.get.bind(":path", p.toString);
252         stmt.get.execute;
253     }
254 
255     /// Returns: All files in the database as relative paths.
256     Path[] getFiles() @trusted {
257         auto stmt = db.prepare(format!"SELECT path FROM %s"(filesTable));
258         auto res = stmt.get.execute;
259 
260         auto app = appender!(Path[]);
261         foreach (ref r; res) {
262             app.put(Path(r.peek!string(0)));
263         }
264 
265         return app.data;
266     }
267 
268     Nullable!Checksum getFileChecksum(const Path p) @trusted {
269         static immutable sql = format!"SELECT checksum0,checksum1 FROM %s WHERE path=:path"(
270                 filesTable);
271         auto stmt = db.prepare(sql);
272         stmt.get.bind(":path", p.toString);
273         auto res = stmt.get.execute;
274 
275         typeof(return) rval;
276         if (!res.empty) {
277             rval = Checksum(res.front.peek!long(0), res.front.peek!long(1));
278         }
279 
280         return rval;
281     }
282 
283     /// Returns: the timestamp of the newest file that was added.
284     Optional!SysTime getNewestFile() @trusted {
285         auto stmt = db.prepare(format!"SELECT timestamp
286             FROM %s ORDER BY datetime(timestamp) DESC LIMIT 1"(
287                 filesTable));
288         auto res = stmt.get.execute;
289 
290         foreach (ref r; res) {
291             return some(r.peek!string(0).fromSqLiteDateTime);
292         }
293 
294         return none!SysTime;
295     }
296 
297     void put(const Path p, Checksum cs, const Language lang, bool isRoot) @trusted {
298         static immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, lang, timestamp, root)
299             VALUES (:path, :checksum0, :checksum1, :lang, :time, :root)"(
300                 filesTable);
301         auto stmt = db.prepare(sql);
302         stmt.get.bind(":path", p.toString);
303         stmt.get.bind(":checksum0", cast(long) cs.c0);
304         stmt.get.bind(":checksum1", cast(long) cs.c1);
305         stmt.get.bind(":lang", cast(long) lang);
306         stmt.get.bind(":time", Clock.currTime.toSqliteDateTime);
307         stmt.get.bind(":root", isRoot);
308         stmt.get.execute;
309     }
310 
311     void put(const TestFile tfile) @trusted {
312         static immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, timestamp)
313             VALUES (:path, :checksum0, :checksum1, :timestamp)"(
314                 testFilesTable);
315         auto stmt = db.prepare(sql);
316         stmt.get.bind(":path", tfile.file.get.toString);
317         stmt.get.bind(":checksum0", cast(long) tfile.checksum.get.c0);
318         stmt.get.bind(":checksum1", cast(long) tfile.checksum.get.c1);
319         stmt.get.bind(":timestamp", tfile.timeStamp.toSqliteDateTime);
320         stmt.get.execute;
321     }
322 
323     TestFile[] getTestFiles() @trusted {
324         static immutable sql = format!"SELECT path,checksum0,checksum1,timestamp FROM %s"(
325                 testFilesTable);
326         auto stmt = db.prepare(sql);
327         auto res = stmt.get.execute;
328 
329         auto app = appender!(TestFile[]);
330         foreach (ref r; res) {
331             app.put(TestFile(TestFilePath(Path(r.peek!string(0))),
332                     TestFileChecksum(Checksum(r.peek!long(1), r.peek!long(2))),
333                     r.peek!string(3).fromSqLiteDateTime));
334         }
335 
336         return app.data;
337     }
338 
339     /// Returns: the oldest test file, if it exists.
340     Optional!TestFile getNewestTestFile() @trusted {
341         auto stmt = db.prepare(format!"SELECT path,checksum0,checksum1,timestamp
342             FROM %s ORDER BY datetime(timestamp) DESC LIMIT 1"(
343                 testFilesTable));
344         auto res = stmt.get.execute;
345 
346         foreach (ref r; res) {
347             return some(TestFile(TestFilePath(Path(r.peek!string(0))),
348                     TestFileChecksum(Checksum(r.peek!long(1), r.peek!long(2))),
349                     r.peek!string(3).fromSqLiteDateTime));
350         }
351 
352         return none!TestFile;
353     }
354 
355     /// Remove the file with all mutations that are coupled to it.
356     void removeFile(const TestFilePath p) @trusted {
357         auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(testFilesTable));
358         stmt.get.bind(":path", p.get.toString);
359         stmt.get.execute;
360     }
361 
362     enum CntAction {
363         /// Increment the counter
364         incr,
365         /// Reset the counter to zero
366         reset,
367     }
368 
369     /** Update the status of a mutant.
370      *
371      * Params:
372      *  id = ID of the mutant
373      *  st = status to broadcast
374      *  d = time spent on veryfing the mutant
375      *  tcs = test cases that killed the mutant
376      *  counter = how to act with the counter
377      */
378     void updateMutation(const MutationId id, const Mutation.Status st,
379             const ExitStatus ecode, const MutantTimeProfile p, const(TestCase)[] tcs) @trusted {
380         static immutable sql = "UPDATE %s SET
381             status=:st,compile_time_ms=:compile,test_time_ms=:test,update_ts=:update_ts
382             WHERE id IN (SELECT st_id FROM %s WHERE id = :id)";
383 
384         auto stmt = db.prepare(format!sql(mutationStatusTable, mutationTable));
385         stmt.get.bind(":st", cast(long) st);
386         stmt.get.bind(":id", id.get);
387         stmt.get.bind(":compile", p.compile.total!"msecs");
388         stmt.get.bind(":test", p.test.total!"msecs");
389         stmt.get.bind(":update_ts", Clock.currTime.toSqliteDateTime);
390         stmt.get.execute;
391 
392         updateMutationTestCases(id, tcs);
393     }
394 
395     /** Update the status of a mutant.
396      *
397      * Params:
398      *  id = ID of the mutant
399      *  st = status to broadcast
400      *  d = time spent on veryfing the mutant
401      *  tcs = test cases that killed the mutant
402      *  counter = how to act with the counter
403      */
404     void updateMutation(const MutationStatusId id, const Mutation.Status st,
405             const ExitStatus ecode, const MutantTimeProfile p) @trusted {
406         static immutable sql = "UPDATE %s SET
407             status=:st,compile_time_ms=:compile,test_time_ms=:test,update_ts=:update_ts
408             WHERE id = :id";
409 
410         auto stmt = db.prepare(format!sql(mutationStatusTable));
411         stmt.get.bind(":id", id.get);
412         stmt.get.bind(":st", cast(long) st);
413         stmt.get.bind(":compile", p.compile.total!"msecs");
414         stmt.get.bind(":test", p.test.total!"msecs");
415         stmt.get.bind(":update_ts", Clock.currTime.toSqliteDateTime);
416         stmt.get.execute;
417     }
418 
419     /// Update the time used to test the mutant.
420     void updateMutation(const MutationStatusId id, const MutantTimeProfile p) @trusted {
421         static immutable sql = format!"UPDATE %s SET compile_time_ms=:compile,test_time_ms=:test WHERE id = :id"(
422                 mutationStatusTable);
423         auto stmt = db.prepare(sql);
424         stmt.get.bind(":id", id.get);
425         stmt.get.bind(":compile", p.compile.total!"msecs");
426         stmt.get.bind(":test", p.test.total!"msecs");
427         stmt.get.execute;
428     }
429 
430     /** Update the status of a mutant.
431      *
432      * Params:
433      *  id = mutation status ID
434      *  st = new status
435      *  update_ts = if the update timestamp should be updated.
436      */
437     void updateMutationStatus(const MutationStatusId id, const Mutation.Status st,
438             const ExitStatus ecode, Flag!"updateTs" update_ts = No.updateTs) @trusted {
439 
440         auto stmt = () {
441             if (update_ts) {
442                 const ts = Clock.currTime.toSqliteDateTime;
443                 auto s = db.prepare(format!"UPDATE %s SET status=:st,update_ts=:update_ts WHERE id=:id"(
444                         mutationStatusTable));
445                 s.get.bind(":update_ts", ts);
446                 return s;
447             } else
448                 return db.prepare(format!"UPDATE %s SET status=:st WHERE id=:id"(
449                         mutationStatusTable));
450         }();
451         stmt.get.bind(":st", st.to!long);
452         stmt.get.bind(":id", id.to!long);
453         stmt.get.execute;
454     }
455 
456     /// Returns: all mutation status IDs.
457     MutationStatusId[] getAllMutationStatus() @trusted {
458         static immutable sql = format!"SELECT id FROM %s"(mutationStatusTable);
459 
460         auto app = appender!(MutationStatusId[])();
461         auto stmt = db.prepare(sql);
462         foreach (r; stmt.get.execute)
463             app.put(MutationStatusId(r.peek!long(0)));
464         return app.data;
465     }
466 
467     // TODO: change to my.optional
468     Nullable!(Mutation.Status) getMutationStatus(const MutationStatusId id) @trusted {
469         static immutable sql = format!"SELECT status FROM %s WHERE id=:id"(mutationStatusTable);
470         auto stmt = db.prepare(sql);
471         stmt.get.bind(":id", id.get);
472 
473         typeof(return) rval;
474         foreach (a; stmt.get.execute) {
475             rval = cast(Mutation.Status) a.peek!long(0);
476         }
477         return rval;
478     }
479 
480     Nullable!MutationEntry getMutation(const MutationId id) @trusted {
481         import dextool.plugin.mutate.backend.type;
482 
483         typeof(return) rval;
484 
485         static immutable get_mut_sql = format("SELECT
486             t0.id,
487             t0.kind,
488             t3.compile_time_ms,
489             t3.test_time_ms,
490             t1.offset_begin,
491             t1.offset_end,
492             t1.line,
493             t1.column,
494             t2.path,
495             t2.lang
496             FROM %s t0,%s t1,%s t2,%s t3
497             WHERE
498             t0.id = :id AND
499             t0.mp_id = t1.id AND
500             t1.file_id = t2.id AND
501             t3.id = t0.st_id
502             ", mutationTable,
503                 mutationPointTable, filesTable, mutationStatusTable);
504 
505         auto stmt = db.prepare(get_mut_sql);
506         stmt.get.bind(":id", cast(long) id);
507         auto res = stmt.get.execute;
508 
509         if (res.empty)
510             return rval;
511 
512         auto v = res.front;
513 
514         auto mp = MutationPoint(Offset(v.peek!uint(4), v.peek!uint(5)));
515         mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))];
516         auto pkey = MutationId(v.peek!long(0));
517         auto file = Path(v.peek!string(8));
518         auto sloc = SourceLoc(v.peek!uint(6), v.peek!uint(7));
519         auto lang = v.peek!long(9).to!Language;
520 
521         rval = MutationEntry(pkey, file, sloc, mp,
522                 MutantTimeProfile(v.peek!long(2).dur!"msecs", v.peek!long(3).dur!"msecs"), lang);
523 
524         return rval;
525     }
526 
527     MutantMetaData getMutantationMetaData(const MutationId id) @trusted {
528         auto rval = MutantMetaData(id);
529         foreach (res; db.run(select!NomutDataTbl.where("mut_id = :mutid", Bind("mutid")), id.get)) {
530             rval.set(NoMut(res.tag, res.comment));
531         }
532         return rval;
533     }
534 
535     // TODO: fix spelling error
536     // TODO: this is a bit inefficient. it should use a callback iterator
537     MutantMetaData[] getMutantationMetaData(const Mutation.Kind[] kinds, const Mutation
538             .Status status) @trusted {
539         const sql = format!"SELECT DISTINCT t.mut_id, t.tag, t.comment
540         FROM %s t, %s t1, %s t2
541         WHERE
542         t.mut_id = t1.id AND
543         t1.st_id = t2.id AND
544         t2.status = :status AND
545         t1.kind IN (%(%s,%))
546         ORDER BY
547         t.mut_id"(nomutDataTable, mutationTable,
548                 mutationStatusTable, kinds.map!(a => cast(long) a));
549         auto stmt = db.prepare(sql);
550         stmt.get.bind(":status", cast(long) status);
551 
552         auto app = appender!(MutantMetaData[])();
553         foreach (res; stmt.get.execute) {
554             app.put(MutantMetaData(MutationId(res.peek!long(0)),
555                     MutantAttr(NoMut(res.peek!string(1), res.peek!string(2)))));
556         }
557         return app.data;
558     }
559 
560     Nullable!Path getPath(const MutationId id) @trusted {
561         static immutable get_path_sql = format("SELECT t2.path
562             FROM
563             %s t0, %s t1, %s t2
564             WHERE
565             t0.id = :id AND
566             t0.mp_id = t1.id AND
567             t1.file_id = t2.id
568             ", mutationTable,
569                 mutationPointTable, filesTable);
570 
571         auto stmt = db.prepare(get_path_sql);
572         stmt.get.bind(":id", cast(long) id);
573         auto res = stmt.get.execute;
574 
575         typeof(return) rval;
576         if (!res.empty)
577             rval = Path(res.front.peek!string(0));
578         return rval;
579     }
580 
581     /// Returns: the mutants that are connected to the mutation statuses.
582     MutantInfo[] getMutantsInfo(const Mutation.Kind[] kinds, const(MutationStatusId)[] id) @trusted {
583         const get_mutid_sql = format("SELECT t0.id,t2.status,t2.exit_code,t0.kind,t1.line,t1.column
584             FROM %s t0,%s t1, %s t2
585             WHERE
586             t0.st_id IN (%(%s,%)) AND
587             t0.st_id = t2.id AND
588             t0.kind IN (%(%s,%)) AND
589             t0.mp_id = t1.id", mutationTable,
590                 mutationPointTable, mutationStatusTable, id.map!(a => a.get),
591                 kinds.map!(a => cast(int) a));
592         auto stmt = db.prepare(get_mutid_sql);
593 
594         auto app = appender!(MutantInfo[])();
595         foreach (res; stmt.get.execute) {
596             app.put(MutantInfo(MutationId(res.peek!long(0)), res.peek!long(1)
597                     .to!(Mutation.Status), res.peek!int(2).ExitStatus,
598                     res.peek!long(3).to!(Mutation.Kind),
599                     SourceLoc(res.peek!uint(4), res.peek!uint(5))));
600         }
601 
602         return app.data;
603     }
604 
605     /// Returns: the mutants that are connected to the mutation statuses.
606     MutationId[] getMutationIds(const(Mutation.Kind)[] kinds, const(MutationStatusId)[] id) @trusted {
607         if (id.length == 0)
608             return null;
609 
610         const get_mutid_sql = format!"SELECT id FROM %s t0
611             WHERE
612             t0.st_id IN (%(%s,%)) AND
613             t0.kind IN (%(%s,%))"(mutationTable,
614                 id.map!(a => cast(long) a), kinds.map!(a => cast(int) a));
615         auto stmt = db.prepare(get_mutid_sql);
616 
617         auto app = appender!(MutationId[])();
618         foreach (res; stmt.get.execute)
619             app.put(MutationId(res.peek!long(0)));
620         return app.data;
621     }
622 
623     Nullable!MutationId getMutationId(const MutationStatusId id) @trusted {
624         static immutable sql = format!"SELECT id FROM %s WHERE st_id=:st_id"(mutationTable);
625         auto stmt = db.prepare(sql);
626         stmt.get.bind(":st_id", id.get);
627 
628         typeof(return) rval;
629         foreach (res; stmt.get.execute) {
630             rval = res.peek!long(0).MutationId;
631             break;
632         }
633         return rval;
634     }
635 
636     Nullable!MutationStatusId getMutationStatusId(const MutationId id) @trusted {
637         static immutable sql = format!"SELECT st_id FROM %s WHERE id=:id"(mutationTable);
638         auto stmt = db.prepare(sql);
639         stmt.get.bind(":id", cast(long) id);
640 
641         typeof(return) rval;
642         foreach (res; stmt.get.execute) {
643             rval = MutationStatusId(res.peek!long(0));
644         }
645         return rval;
646     }
647 
648     Nullable!MutationStatusId getMutationStatusId(const Checksum cs) @trusted {
649         static immutable sql = format!"SELECT id FROM %s WHERE checksum0=:cs0 AND checksum1=:cs1"(
650                 mutationStatusTable);
651         auto stmt = db.prepare(sql);
652         stmt.get.bind(":cs0", cast(long) cs.c0);
653         stmt.get.bind(":cs1", cast(long) cs.c1);
654 
655         typeof(return) rval;
656         foreach (res; stmt.get.execute) {
657             rval = MutationStatusId(res.peek!long(0));
658         }
659         return rval;
660     }
661 
662     // TODO: maybe this need to return the exit code too?
663     // Returns: the status of the mutant
664     Nullable!(Mutation.Status) getMutationStatus(const MutationId id) @trusted {
665         auto s = format!"SELECT status FROM %s WHERE id IN (SELECT st_id FROM %s WHERE id=:mut_id)"(
666                 mutationStatusTable, mutationTable);
667         auto stmt = db.prepare(s);
668         stmt.get.bind(":mut_id", cast(long) id);
669         typeof(return) rval;
670         foreach (res; stmt.get.execute)
671             rval = res.peek!long(0).to!(Mutation.Status);
672         return rval;
673     }
674 
675     /// Returns: the mutants in the file at the line.
676     MutationStatusId[] getMutationsOnLine(const(Mutation.Kind)[] kinds, FileId fid, SourceLoc sloc) @trusted {
677         // TODO: should it also be line_end?
678         const sql = format("SELECT DISTINCT t0.id FROM %s t0, %s t1, %s t2
679                     WHERE
680                     t1.st_id = t0.id AND
681                     t1.kind IN (%(%s,%)) AND
682                     t1.mp_id = t2.id AND
683                     t2.file_id = :fid AND
684                     (:line BETWEEN t2.line AND t2.line_end)",
685                 mutationStatusTable, mutationTable, mutationPointTable,
686                 kinds.map!(a => cast(int) a));
687         auto stmt = db.prepare(sql);
688         stmt.get.bind(":fid", cast(long) fid);
689         stmt.get.bind(":line", sloc.line);
690 
691         auto app = appender!(typeof(return))();
692         foreach (res; stmt.get.execute)
693             app.put(MutationStatusId(res.peek!long(0)));
694         return app.data;
695     }
696 
697     LineMetadata getLineMetadata(const FileId fid, const SourceLoc sloc) @trusted {
698         // TODO: change this select to using microrm
699         static immutable sql = format("SELECT nomut,tag,comment FROM %s
700             WHERE
701             file_id = :fid AND
702             line = :line", rawSrcMetadataTable);
703         auto stmt = db.prepare(sql);
704         stmt.get.bind(":fid", cast(long) fid);
705         stmt.get.bind(":line", sloc.line);
706 
707         auto rval = typeof(return)(fid, sloc.line);
708         foreach (res; stmt.get.execute) {
709             if (res.peek!long(0) != 0)
710                 rval.set(NoMut(res.peek!string(1), res.peek!string(2)));
711         }
712 
713         return rval;
714     }
715 
716     /// Returns: the `nr` mutants that where the longst since they where tested.
717     MutationStatusTime[] getOldestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted {
718         const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1
719                     WHERE
720                     t0.update_ts IS NOT NULL AND
721                     t1.st_id = t0.id AND
722                     t1.kind IN (%(%s,%))
723                     ORDER BY datetime(t0.update_ts) ASC LIMIT :limit",
724                 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a));
725         auto stmt = db.prepare(sql);
726         stmt.get.bind(":limit", nr);
727 
728         auto app = appender!(MutationStatusTime[])();
729         foreach (res; stmt.get.execute)
730             app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)),
731                     res.peek!string(1).fromSqLiteDateTime));
732         return app.data;
733     }
734 
735     /// Returns: the `nr` mutants that where last tested.
736     MutationStatusTime[] getLatestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted {
737         const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1
738                     WHERE
739                     t0.update_ts IS NOT NULL AND
740                     t1.st_id = t0.id AND
741                     t1.kind IN (%(%s,%))
742                     ORDER BY datetime(t0.update_ts) DESC LIMIT :limit",
743                 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a));
744         auto stmt = db.prepare(sql);
745         stmt.get.bind(":limit", nr);
746 
747         auto app = appender!(MutationStatusTime[])();
748         foreach (res; stmt.get.execute)
749             app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)),
750                     res.peek!string(1).fromSqLiteDateTime));
751         return app.data;
752     }
753 
754     /// Returns: the `nr` mutant with the highest count that has not been killed and existed in the system the longest.
755     MutationStatus[] getHighestPrioMutant(const(Mutation.Kind)[] kinds,
756             const Mutation.Status status, const long nr) @trusted {
757         const sql = format("SELECT t0.id,t0.status,t0.prio,t0.update_ts,t0.added_ts
758             FROM %s t0, %s t1
759             WHERE
760             t0.update_ts IS NOT NULL AND
761             t0.status = :status AND
762             t1.st_id = t0.id AND
763             t1.kind IN (%(%s,%)) AND
764             t1.st_id NOT IN (SELECT st_id FROM %s WHERE nomut != 0)
765             ORDER BY t0.prio DESC LIMIT :limit",
766                 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a), srcMetadataTable);
767         auto stmt = db.prepare(sql);
768         stmt.get.bind(":status", cast(long) status);
769         stmt.get.bind(":limit", nr);
770 
771         auto app = appender!(MutationStatus[])();
772         foreach (res; stmt.get.execute) {
773             auto added = () {
774                 auto raw = res.peek!string(4);
775                 if (raw.length == 0)
776                     return Nullable!SysTime();
777                 return Nullable!SysTime(raw.fromSqLiteDateTime);
778             }();
779 
780             // dfmt off
781             app.put(MutationStatus(
782                 MutationStatusId(res.peek!long(0)),
783                 res.peek!long(1).to!(Mutation.Status),
784                 res.peek!long(2).MutantPrio,
785                 res.peek!string(3).fromSqLiteDateTime,
786                 added,
787             ));
788             // dfmt on
789         }
790 
791         return app.data;
792     }
793 
794     /** Get SourceLoc for a specific mutation id.
795      */
796     Nullable!SourceLoc getSourceLocation(MutationId id) @trusted {
797         auto s = format!"SELECT line, column FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE id=:mut_id)"(
798                 mutationPointTable, mutationTable);
799         auto stmt = db.prepare(s);
800         stmt.get.bind(":mut_id", cast(long) id);
801         typeof(return) rval;
802         foreach (res; stmt.get.execute)
803             rval = SourceLoc(res.peek!uint(0), res.peek!uint(1));
804         return rval;
805     }
806 
807     /** Remove all mutations of kinds.
808      */
809     void removeMutant(const Mutation.Kind[] kinds) @trusted {
810         const s = format!"DELETE FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE kind IN (%(%s,%)))"(
811                 mutationPointTable, mutationTable, kinds.map!(a => cast(int) a));
812         auto stmt = db.prepare(s);
813         stmt.get.execute;
814     }
815 
816     /** Reset all mutations of kinds with the status `st` to unknown.
817      */
818     void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted {
819         const s = format!"UPDATE %s SET status=%s WHERE status = %s AND id IN(SELECT st_id FROM %s WHERE kind IN (%(%s,%)))"(
820                 mutationStatusTable, to_st.to!long, st.to!long,
821                 mutationTable, kinds.map!(a => cast(int) a));
822         auto stmt = db.prepare(s);
823         stmt.get.execute;
824     }
825 
826     /** Mark a mutant with status and rationale (also adds metadata).
827      */
828     void markMutant(const MutationId id, const Path file, const SourceLoc sloc,
829             const MutationStatusId statusId, const Checksum cs,
830             const Mutation.Status s, const Rationale r, string mutationTxt) @trusted {
831         db.run(insertOrReplace!MarkedMutantTbl, MarkedMutantTbl(cs.c0, cs.c1,
832                 statusId.get, id.get, sloc.line, sloc.column, file, s,
833                 Clock.currTime.toUTC, r.get, mutationTxt));
834     }
835 
836     void removeMarkedMutant(const Checksum cs) @trusted {
837         db.run(delete_!MarkedMutantTbl.where("checksum0 = :cs0", Bind("cs0")), cast(long) cs.c0);
838     }
839 
840     void removeMarkedMutant(const MutationStatusId id) @trusted {
841         db.run(delete_!MarkedMutantTbl.where("st_id = :st_id", Bind("st_id")), id.get);
842     }
843 
844     /// Returns: All mutants with that are marked orderd by their path
845     MarkedMutant[] getMarkedMutants() @trusted {
846         import miniorm : OrderingTermSort;
847 
848         auto app = appender!(MarkedMutant[])();
849         foreach (m; db.run(select!MarkedMutantTbl.orderBy(OrderingTermSort.ASC, [
850                     "path"
851                 ]))) {
852             app.put(.make(m));
853         }
854 
855         return app.data;
856     }
857 
858     Mutation.Kind getKind(MutationId id) @trusted {
859         static immutable sql = format!"SELECT kind FROM %s WHERE id=:id"(mutationTable);
860         auto stmt = db.prepare(sql);
861         stmt.get.bind(":id", cast(long) id);
862 
863         typeof(return) rval;
864         foreach (res; stmt.get.execute) {
865             rval = res.peek!long(0).to!(Mutation.Kind);
866         }
867         return rval;
868     }
869 
870     import dextool.plugin.mutate.backend.type;
871 
872     alias aliveSrcMutants = countMutants!([
873             Mutation.Status.alive, Mutation.Status.noCoverage
874             ]);
875     alias killedSrcMutants = countMutants!([Mutation.Status.killed]);
876     alias timeoutSrcMutants = countMutants!([Mutation.Status.timeout]);
877     alias noCovSrcMutants = countMutants!([Mutation.Status.noCoverage]);
878     alias equivalentMutants = countMutants!([Mutation.Status.equivalent]);
879 
880     /// Returns: Total that should be counted when calculating the mutation score.
881     alias totalSrcMutants = countMutants!([
882             Mutation.Status.alive, Mutation.Status.killed,
883             Mutation.Status.timeout, Mutation.Status.noCoverage
884             ]);
885 
886     alias unknownSrcMutants = countMutants!([Mutation.Status.unknown]);
887     alias killedByCompilerSrcMutants = countMutants!([
888             Mutation.Status.killedByCompiler
889             ]);
890 
891     /** Count the mutants with the nomut metadata.
892      *
893      * Params:
894      *  status = status the mutants must be in to be counted.
895      *  distinc = count based on unique source code changes.
896      *  kinds = the kind of mutants to count.
897      *  file = file to count mutants in.
898      */
899     private MutationReportEntry countMutants(int[] status)(const Mutation.Kind[] kinds,
900             string file = null) @trusted {
901         const qq = "
902             SELECT count(*),sum(compile_time_ms),sum(test_time_ms)
903             FROM (
904             SELECT sum(t1.compile_time_ms) compile_time_ms,sum(t1.test_time_ms) test_time_ms
905             FROM %s t0, %s t1%s
906             WHERE
907             %s
908             t0.st_id = t1.id AND
909             t1.status IN (%(%s,%)) AND
910             t0.kind IN (%(%s,%))
911             GROUP BY t1.id)";
912         const query = () {
913             auto fq = file.length == 0
914                 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND";
915             auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3",
916                     mutationPointTable, filesTable);
917             return format(qq, mutationTable, mutationStatusTable, fq_from, fq,
918                     status, kinds.map!(a => cast(int) a));
919         }();
920 
921         typeof(return) rval;
922         auto stmt = db.prepare(query);
923         if (file.length != 0)
924             stmt.get.bind(":path", file);
925         auto res = stmt.get.execute;
926         if (!res.empty) {
927             rval = MutationReportEntry(res.front.peek!long(0),
928                     MutantTimeProfile(res.front.peek!long(1).dur!"msecs",
929                         res.front.peek!long(2).dur!"msecs"));
930         }
931         return rval;
932     }
933 
934     /** Count the mutants with the nomut metadata.
935      *
936      * Params:
937      *  status = status the mutants must be in to be counted.
938      *  distinc = count based on unique source code changes.
939      *  kinds = the kind of mutants to count.
940      *  file = file to count mutants in.
941      */
942     private MetadataNoMutEntry countNoMutMutants(int[] status, bool distinct)(
943             const Mutation.Kind[] kinds, string file = null) @trusted {
944         static if (distinct) {
945             auto sql_base = "
946                 SELECT count(*)
947                 FROM (
948                 SELECT count(*)
949                 FROM %s t0, %s t1,%s t4%s
950                 WHERE
951                 %s
952                 t0.st_id = t1.id AND
953                 t0.st_id = t4.st_id AND
954                 t4.nomut != 0 AND
955                 t1.status IN (%(%s,%)) AND
956                 t0.kind IN (%(%s,%))
957                 GROUP BY t1.id)";
958         } else {
959             auto sql_base = "
960                 SELECT count(*)
961                 FROM %s t0, %s t1,%s t4%s
962                 WHERE
963                 %s
964                 t0.st_id = t1.id AND
965                 t0.st_id = t4.st_id AND
966                 t4.nomut != 0 AND
967                 t1.status IN (%(%s,%)) AND
968                 t0.kind IN (%(%s,%))";
969         }
970         const query = () {
971             auto fq = file.length == 0
972                 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND";
973             auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3",
974                     mutationPointTable, filesTable);
975             return format(sql_base, mutationTable, mutationStatusTable,
976                     srcMetadataTable, fq_from, fq, status, kinds.map!(a => cast(int) a));
977         }();
978 
979         typeof(return) rval;
980         auto stmt = db.prepare(query);
981         if (file.length != 0)
982             stmt.get.bind(":path", file);
983         auto res = stmt.get.execute;
984         if (!res.empty)
985             rval = MetadataNoMutEntry(res.front.peek!long(0));
986         return rval;
987     }
988 
989     /// ditto.
990     alias aliveNoMutSrcMutants = countNoMutMutants!([Mutation.Status.alive], true);
991 
992     /// Returns: mutants killed by the test case.
993     MutationStatusId[] testCaseKilledSrcMutants(const Mutation.Kind[] kinds, TestCase tc) @trusted {
994         const query = format("
995             SELECT t1.id
996             FROM %s t0, %s t1, %s t2, %s t3
997             WHERE
998             t0.st_id = t1.id AND
999             t1.status = :st AND
1000             t0.kind IN (%(%s,%)) AND
1001             t2.name = :name AND
1002             t2.id = t3.tc_id AND
1003             t3.st_id = t1.id
1004             GROUP BY t1.id", mutationTable, mutationStatusTable,
1005                 allTestCaseTable, killedTestCaseTable, kinds.map!(a => cast(int) a));
1006 
1007         auto stmt = db.prepare(query);
1008         stmt.get.bind(":st", cast(long) Mutation.Status.killed);
1009         stmt.get.bind(":name", tc.name);
1010 
1011         auto app = appender!(MutationStatusId[])();
1012         foreach (res; stmt.get.execute)
1013             app.put(MutationStatusId(res.peek!long(0)));
1014 
1015         return app.data;
1016     }
1017 
1018     /// Returns: mutants at mutations points that the test case has killed mutants at.
1019     alias testCaseMutationPointAliveSrcMutants = testCaseCountSrcMutants!([
1020             Mutation.Status.alive
1021             ]);
1022     /// ditto
1023     alias testCaseMutationPointTimeoutSrcMutants = testCaseCountSrcMutants!(
1024             [Mutation.Status.timeout]);
1025     /// ditto
1026     alias testCaseMutationPointKilledSrcMutants = testCaseCountSrcMutants!([
1027             Mutation.Status.killed
1028             ]);
1029     /// ditto
1030     alias testCaseMutationPointUnknownSrcMutants = testCaseCountSrcMutants!(
1031             [Mutation.Status.unknown]);
1032     /// ditto
1033     alias testCaseMutationPointKilledByCompilerSrcMutants = testCaseCountSrcMutants!(
1034             [Mutation.Status.killedByCompiler]);
1035     /// ditto
1036     alias testCaseMutationPointTotalSrcMutants = testCaseCountSrcMutants!(
1037             [
1038             Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout
1039             ]);
1040 
1041     private MutationStatusId[] testCaseCountSrcMutants(int[] status)(
1042             const Mutation.Kind[] kinds, TestCase tc) @trusted {
1043         const query = format("
1044             SELECT t1.id
1045             FROM %s t0, %s t1
1046             WHERE
1047             t0.mp_id IN (SELECT t1.id
1048                       FROM %s t0,%s t1, %s t2, %s t3
1049                       WHERE
1050                       t0.mp_id = t1.id AND
1051                       t2.name = :name AND
1052                       t2.id = t3.tc_id AND
1053                       t3.st_id = t0.st_id
1054                       )
1055             AND
1056             t0.st_id = t1.id AND
1057             t1.status IN (%(%s,%)) AND
1058             t0.kind IN (%(%s,%))
1059             GROUP BY t1.id", mutationTable, mutationStatusTable, mutationTable, mutationPointTable,
1060                 allTestCaseTable, killedTestCaseTable, status, kinds.map!(a => cast(int) a));
1061 
1062         auto stmt = db.prepare(query);
1063         stmt.get.bind(":name", tc.name);
1064 
1065         auto app = appender!(MutationStatusId[])();
1066         foreach (res; stmt.get.execute)
1067             app.put(MutationStatusId(res.peek!long(0)));
1068 
1069         return app.data;
1070     }
1071 
1072     Nullable!Checksum getChecksum(MutationStatusId id) @trusted {
1073         static immutable sql = format!"SELECT checksum0, checksum1 FROM %s WHERE id=:id"(
1074                 mutationStatusTable);
1075         auto stmt = db.prepare(sql);
1076         stmt.get.bind(":id", id.get);
1077 
1078         typeof(return) rval;
1079         foreach (res; stmt.get.execute) {
1080             rval = Checksum(res.peek!long(0), res.peek!long(1));
1081             break;
1082         }
1083         return rval;
1084     }
1085 
1086     /// Remove all metadata.
1087     void clearMetadata() {
1088         static immutable sql = format!"DELETE FROM %s"(rawSrcMetadataTable);
1089         db.run(sql);
1090     }
1091 
1092     /** Save line metadata to the database which is used to associate line
1093      * metadata with mutants.
1094      */
1095     void put(const LineMetadata[] mdata) {
1096         import sumtype;
1097 
1098         if (mdata.empty)
1099             return;
1100 
1101         // TODO: convert to microrm
1102         static immutable sql = format!"INSERT OR IGNORE INTO %s
1103             (file_id, line, nomut, tag, comment)
1104             VALUES(:fid, :line, :nomut, :tag, :comment)"(
1105                 rawSrcMetadataTable);
1106 
1107         auto stmt = db.prepare(sql);
1108         foreach (meta; mdata) {
1109             auto nomut = meta.attr.match!((NoMetadata a) => NoMut.init, (NoMut a) => a);
1110             stmt.get.bindAll(cast(long) meta.id, meta.line, meta.isNoMut,
1111                     nomut.tag, nomut.comment);
1112             stmt.get.execute;
1113             stmt.get.reset;
1114         }
1115     }
1116 
1117     /// Store all found mutants.
1118     void put(MutationPointEntry2[] mps, AbsolutePath root) @trusted {
1119         if (mps.empty)
1120             return;
1121 
1122         static immutable insert_mp_sql = format("INSERT OR IGNORE INTO %s
1123             (file_id, offset_begin, offset_end, line, column, line_end, column_end)
1124             SELECT id,:begin,:end,:line,:column,:line_end,:column_end
1125             FROM %s
1126             WHERE
1127             path = :path",
1128                 mutationPointTable, filesTable);
1129         auto mp_stmt = db.prepare(insert_mp_sql);
1130 
1131         foreach (mp; mps) {
1132             auto rel_file = relativePath(mp.file, root).Path;
1133             mp_stmt.get.bind(":begin", mp.offset.begin);
1134             mp_stmt.get.bind(":end", mp.offset.end);
1135             mp_stmt.get.bind(":line", mp.sloc.line);
1136             mp_stmt.get.bind(":column", mp.sloc.column);
1137             mp_stmt.get.bind(":line_end", mp.slocEnd.line);
1138             mp_stmt.get.bind(":column_end", mp.slocEnd.column);
1139             mp_stmt.get.bind(":path", cast(string) rel_file);
1140             mp_stmt.get.execute;
1141             mp_stmt.get.reset;
1142         }
1143 
1144         static immutable insert_cmut_sql = format("INSERT OR IGNORE INTO %s
1145             (status,exit_code,compile_time_ms,test_time_ms,update_ts,added_ts,checksum0,checksum1,prio)
1146             VALUES(:st,0,0,0,:update_ts,:added_ts,:c0,:c1,:prio)",
1147                 mutationStatusTable);
1148         auto cmut_stmt = db.prepare(insert_cmut_sql);
1149         const ts = Clock.currTime.toSqliteDateTime;
1150         cmut_stmt.get.bind(":st", Mutation.Status.unknown);
1151         cmut_stmt.get.bind(":update_ts", ts);
1152         cmut_stmt.get.bind(":added_ts", ts);
1153         foreach (mp; mps) {
1154             const prio = (mp.offset.begin < mp.offset.end) ? mp.offset.end - mp.offset.begin : 0;
1155             foreach (cm; mp.cms) {
1156                 cmut_stmt.get.bind(":c0", cast(long) cm.id.c0);
1157                 cmut_stmt.get.bind(":c1", cast(long) cm.id.c1);
1158                 cmut_stmt.get.bind(":prio", prio);
1159                 cmut_stmt.get.execute;
1160                 cmut_stmt.get.reset;
1161             }
1162         }
1163 
1164         static immutable insert_m_sql = format("INSERT OR IGNORE INTO %s
1165             (mp_id, st_id, kind)
1166             SELECT t0.id,t1.id,:kind FROM %s t0, %s t1, %s t2 WHERE
1167             t2.path = :path AND
1168             t0.file_id = t2.id AND
1169             t0.offset_begin = :off_begin AND
1170             t0.offset_end = :off_end AND
1171             t1.checksum0 = :c0 AND
1172             t1.checksum1 = :c1",
1173                 mutationTable, mutationPointTable, mutationStatusTable, filesTable);
1174         auto insert_m = db.prepare(insert_m_sql);
1175 
1176         foreach (mp; mps) {
1177             foreach (m; mp.cms) {
1178                 auto rel_file = relativePath(mp.file, root).Path;
1179                 insert_m.get.bind(":path", cast(string) rel_file);
1180                 insert_m.get.bind(":off_begin", mp.offset.begin);
1181                 insert_m.get.bind(":off_end", mp.offset.end);
1182                 insert_m.get.bind(":c0", cast(long) m.id.c0);
1183                 insert_m.get.bind(":c1", cast(long) m.id.c1);
1184                 insert_m.get.bind(":kind", m.mut.kind);
1185                 insert_m.get.execute;
1186                 insert_m.get.reset;
1187             }
1188         }
1189     }
1190 
1191     /** Remove all mutants points from the database.
1192      *
1193      * This removes all the mutants because of the cascade delete of the
1194      * tables. But it will keep the mutation statuses and thus the checksums
1195      * and the status of the code changes.
1196      *
1197      * This then mean that when mutations+mutation points are added back they
1198      * may reconnect with a mutation status.
1199      */
1200     void removeAllMutationPoints() @trusted {
1201         static immutable sql = format!"DELETE FROM %s"(mutationPointTable);
1202         db.run(sql);
1203     }
1204 
1205     /// ditto
1206     void removeAllFiles() @trusted {
1207         static immutable sql = format!"DELETE FROM %s"(filesTable);
1208         db.run(sql);
1209     }
1210 
1211     /// Remove mutants that have no connection to a mutation point, orphaned mutants.
1212     void removeOrphanedMutants() @trusted {
1213         import std.datetime.stopwatch : StopWatch, AutoStart;
1214 
1215         const removeIds = () {
1216             static immutable sql = format!"SELECT id FROM %1$s WHERE id NOT IN (SELECT st_id FROM %2$s)"(
1217                     mutationStatusTable, mutationTable);
1218             auto stmt = db.prepare(sql);
1219             auto removeIds = appender!(long[])();
1220             foreach (res; stmt.get.execute)
1221                 removeIds.put(res.peek!long(0));
1222             return removeIds.data;
1223         }();
1224 
1225         immutable batchNr = 1000;
1226         static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(mutationStatusTable);
1227         auto stmt = db.prepare(sql);
1228         auto sw = StopWatch(AutoStart.yes);
1229         foreach (const i, const id; removeIds) {
1230             stmt.get.bind(":id", id);
1231             stmt.get.execute;
1232             stmt.get.reset;
1233 
1234             // continuously print to inform the user of the progress and avoid
1235             // e.g. timeout on jenkins.
1236             if (i > 0 && i % batchNr == 0) {
1237                 const avg = cast(long)(cast(double) sw.peek.total!"msecs" / cast(double) batchNr);
1238                 const t = dur!"msecs"(avg * (removeIds.length - i));
1239                 logger.infof("%s/%s removed (average %sms) (%s) (%s)", i,
1240                         removeIds.length, avg, t, (Clock.currTime + t).toSimpleString);
1241                 sw.reset;
1242             }
1243         }
1244 
1245         logger.infof(!removeIds.empty, "%1$s/%1$s removed", removeIds.length);
1246     }
1247 
1248     /** Add a link between the mutation and what test case killed it.
1249      *
1250      * Params:
1251      *  id = ?
1252      *  tcs = test cases to add
1253      */
1254     void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted {
1255         if (tcs.length == 0)
1256             return;
1257 
1258         immutable statusId = () {
1259             static immutable st_id_for_mutation_q = format!"SELECT st_id FROM %s WHERE id=:id"(
1260                     mutationTable);
1261             auto stmt = db.prepare(st_id_for_mutation_q);
1262             stmt.get.bind(":id", cast(long) id);
1263             return stmt.get.execute.oneValue!long;
1264         }();
1265         updateMutationTestCases(MutationStatusId(statusId), tcs);
1266     }
1267 
1268     /** Add a link between the mutation and what test case killed it.
1269      *
1270      * Params:
1271      *  id = ?
1272      *  tcs = test cases to add
1273      */
1274     void updateMutationTestCases(const MutationStatusId statusId, const(TestCase)[] tcs) @trusted {
1275         if (tcs.length == 0)
1276             return;
1277 
1278         try {
1279             static immutable remove_old_sql = format!"DELETE FROM %s WHERE st_id=:id"(
1280                     killedTestCaseTable);
1281             auto stmt = db.prepare(remove_old_sql);
1282             stmt.get.bind(":id", statusId.get);
1283             stmt.get.execute;
1284         } catch (Exception e) {
1285         }
1286 
1287         static immutable add_if_non_exist_tc_sql = format!"INSERT OR IGNORE INTO %s (name) SELECT :name1 WHERE NOT EXISTS (SELECT * FROM %s WHERE name = :name2)"(
1288                 allTestCaseTable, allTestCaseTable);
1289         auto stmt_insert_tc = db.prepare(add_if_non_exist_tc_sql);
1290 
1291         static immutable add_new_sql = format!"INSERT OR IGNORE INTO %s (st_id, tc_id, location) SELECT :st_id,t1.id,:loc FROM %s t1 WHERE t1.name = :tc"(
1292                 killedTestCaseTable, allTestCaseTable);
1293         auto stmt_insert = db.prepare(add_new_sql);
1294         foreach (const tc; tcs) {
1295             try {
1296                 stmt_insert_tc.get.reset;
1297                 stmt_insert_tc.get.bind(":name1", tc.name);
1298                 stmt_insert_tc.get.bind(":name2", tc.name);
1299                 stmt_insert_tc.get.execute;
1300 
1301                 stmt_insert.get.reset;
1302                 stmt_insert.get.bind(":st_id", statusId.get);
1303                 stmt_insert.get.bind(":loc", tc.location);
1304                 stmt_insert.get.bind(":tc", tc.name);
1305                 stmt_insert.get.execute;
1306             } catch (Exception e) {
1307                 logger.warning(e.msg);
1308             }
1309         }
1310     }
1311 
1312     /** Set detected test cases.
1313      *
1314      * This will replace those that where previously stored.
1315      *
1316      * Returns: ID of affected mutation statuses.
1317      */
1318     MutationStatusId[] setDetectedTestCases(const(TestCase)[] tcs) @trusted {
1319         if (tcs.length == 0)
1320             return null;
1321 
1322         auto ids = appender!(MutationStatusId[])();
1323 
1324         static immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string;
1325         internalAddDetectedTestCases(tcs, tmp_name);
1326 
1327         static immutable mut_st_id = format!"SELECT DISTINCT t1.st_id
1328             FROM %s t0, %s t1
1329             WHERE
1330             t0.name NOT IN (SELECT name FROM %s) AND
1331             t0.id = t1.tc_id"(allTestCaseTable,
1332                 killedTestCaseTable, tmp_name);
1333         auto stmt = db.prepare(mut_st_id);
1334         foreach (res; stmt.get.execute) {
1335             ids.put(res.peek!long(0).MutationStatusId);
1336         }
1337 
1338         static immutable remove_old_sql = format!"DELETE FROM %s WHERE name NOT IN (SELECT name FROM %s)"(
1339                 allTestCaseTable, tmp_name);
1340         db.run(remove_old_sql);
1341 
1342         db.run(format!"DROP TABLE %s"(tmp_name));
1343 
1344         return ids.data;
1345     }
1346 
1347     /** Add test cases to those that have been detected.
1348      *
1349      * They will be added if they are unique.
1350      */
1351     void addDetectedTestCases(const(TestCase)[] tcs) @trusted {
1352         if (tcs.length == 0)
1353             return;
1354 
1355         static immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string;
1356         internalAddDetectedTestCases(tcs, tmp_name);
1357         db.run(format!"DROP TABLE %s"(tmp_name));
1358     }
1359 
1360     /// ditto.
1361     private void internalAddDetectedTestCases(const(TestCase)[] tcs, string tmp_tbl) @trusted {
1362         db.run(format!"CREATE TEMP TABLE %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"(
1363                 tmp_tbl));
1364 
1365         const add_tc_sql = format!"INSERT OR IGNORE INTO %s (name) VALUES(:name)"(tmp_tbl);
1366         auto insert_s = db.prepare(add_tc_sql);
1367         foreach (tc; tcs.filter!(a => !a.name.empty)) {
1368             insert_s.get.bind(":name", tc.name);
1369             insert_s.get.execute;
1370             insert_s.get.reset;
1371         }
1372 
1373         // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table
1374         //Q: What is happening here?
1375         //
1376         //A: Conceptually, we select all rows from table1 and for each row we
1377         //attempt to find a row in table2 with the same value for the name
1378         //column.  If there is no such row, we just leave the table2 portion of
1379         //our result empty for that row. Then we constrain our selection by
1380         //picking only those rows in the result where the matching row does not
1381         //exist. Finally, We ignore all fields from our result except for the
1382         //name column (the one we are sure that exists, from table1).
1383         //
1384         //While it may not be the most performant method possible in all cases,
1385         //it should work in basically every database engine ever that attempts
1386         //to implement ANSI 92 SQL
1387         const add_missing_sql = format!"INSERT OR IGNORE INTO %s (name) SELECT t1.name FROM %s t1 LEFT JOIN %s t2 ON t2.name = t1.name WHERE t2.name IS NULL"(
1388                 allTestCaseTable, tmp_tbl, allTestCaseTable);
1389         db.run(add_missing_sql);
1390     }
1391 
1392     /// Returns: detected test cases.
1393     TestCase[] getDetectedTestCases() @trusted {
1394         auto rval = appender!(TestCase[])();
1395         db.run(select!AllTestCaseTbl).map!(a => TestCase(a.name)).copy(rval);
1396         return rval.data;
1397     }
1398 
1399     /// Returns: detected test cases.
1400     TestCaseId[] getDetectedTestCaseIds() @trusted {
1401         auto rval = appender!(TestCaseId[])();
1402         db.run(select!AllTestCaseTbl).map!(a => TestCaseId(a.id)).copy(rval);
1403         return rval.data;
1404     }
1405 
1406     /// Returns: test cases that has killed zero mutants.
1407     TestCase[] getTestCasesWithZeroKills() @trusted {
1408         static immutable sql = format("SELECT t1.name FROM %s t1 WHERE t1.id NOT IN (SELECT tc_id FROM %s)",
1409                 allTestCaseTable, killedTestCaseTable);
1410 
1411         auto rval = appender!(TestCase[])();
1412         auto stmt = db.prepare(sql);
1413         foreach (a; stmt.get.execute)
1414             rval.put(TestCase(a.peek!string(0)));
1415 
1416         return rval.data;
1417     }
1418 
1419     /** Guarantees that the there are no duplications of `TestCaseId`.
1420      *
1421      * Returns: test cases that has killed at least one mutant.
1422      */
1423     TestCaseId[] getTestCasesWithAtLeastOneKill(const Mutation.Kind[] kinds) @trusted {
1424         const sql = format!"SELECT DISTINCT t1.id
1425             FROM %s t1, %s t2, %s t3
1426             WHERE
1427             t1.id = t2.tc_id AND
1428             t2.st_id == t3.st_id AND
1429             t3.kind IN (%(%s,%))"(allTestCaseTable,
1430                 killedTestCaseTable, mutationTable, kinds.map!(a => cast(int) a));
1431 
1432         auto rval = appender!(TestCaseId[])();
1433         auto stmt = db.prepare(sql);
1434         foreach (a; stmt.get.execute)
1435             rval.put(TestCaseId(a.peek!long(0)));
1436 
1437         return rval.data;
1438     }
1439 
1440     /// Returns: the name of the test case.
1441     string getTestCaseName(const TestCaseId id) @trusted {
1442         static immutable sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable);
1443         auto stmt = db.prepare(sql);
1444         stmt.get.bind(":id", cast(long) id);
1445         auto res = stmt.get.execute;
1446         return res.oneValue!string;
1447     }
1448 
1449     /// Returns: stats about the test case.
1450     Nullable!TestCaseInfo getTestCaseInfo(const TestCase tc, const Mutation.Kind[] kinds) @trusted {
1451         const sql = format("SELECT sum(ctime),sum(ttime),count(*)
1452             FROM (
1453             SELECT sum(t2.compile_time_ms) ctime,sum(t2.test_time_ms) ttime
1454             FROM %s t0, %s t1, %s t2, %s t3
1455             WHERE
1456             t0.name = :name AND
1457             t0.id = t1.tc_id AND
1458             t1.st_id = t2.id AND
1459             t1.st_id = t3.st_id AND
1460             t3.kind IN (%(%s,%))
1461             GROUP BY t1.st_id)", allTestCaseTable,
1462                 killedTestCaseTable, mutationStatusTable, mutationTable,
1463                 kinds.map!(a => cast(int) a));
1464         auto stmt = db.prepare(sql);
1465         stmt.get.bind(":name", tc.name);
1466 
1467         typeof(return) rval;
1468         foreach (a; stmt.get.execute) {
1469             rval = TestCaseInfo(MutantTimeProfile(a.peek!long(0).dur!"msecs",
1470                     a.peek!long(1).dur!"msecs"), a.peek!long(2));
1471         }
1472         return rval;
1473     }
1474 
1475     /// Returns: all test cases for the file and the mutants they killed.
1476     TestCaseInfo2[] getAllTestCaseInfo2(const FileId file, const Mutation.Kind[] kinds) @trusted {
1477         // row of test case name and mutation id.
1478         const sql = format("SELECT t0.name,t3.id
1479             FROM %s t0, %s t1, %s t2, %s t3, %s t4
1480             WHERE
1481             t0.id = t1.tc_id AND
1482             t1.st_id = t2.id AND
1483             t2.id = t3.st_id AND
1484             t4.id = :file_id AND
1485             t3.kind IN (%(%s,%))", allTestCaseTable, killedTestCaseTable,
1486                 mutationStatusTable, mutationTable, filesTable, kinds.map!(a => cast(int) a));
1487         auto stmt = db.prepare(sql);
1488         stmt.get.bind(":file_id", cast(long) file);
1489 
1490         MutationId[][string] data;
1491         foreach (row; stmt.get.execute) {
1492             const name = row.peek!string(0);
1493             if (auto v = name in data) {
1494                 *v ~= MutationId(row.peek!long(1));
1495             } else {
1496                 data[name] = [MutationId(row.peek!long(1))];
1497             }
1498         }
1499 
1500         auto app = appender!(TestCaseInfo2[])();
1501         data.byKeyValue.map!(a => TestCaseInfo2(TestCase(a.key), a.value)).copy(app);
1502         return app.data;
1503     }
1504 
1505     /// Returns: the test case.
1506     Nullable!TestCase getTestCase(const TestCaseId id) @trusted {
1507         static immutable sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable);
1508         auto stmt = db.prepare(sql);
1509         stmt.get.bind(":id", cast(long) id);
1510 
1511         typeof(return) rval;
1512         foreach (res; stmt.get.execute) {
1513             rval = TestCase(res.peek!string(0));
1514         }
1515         return rval;
1516     }
1517 
1518     /// Returns: the test case id.
1519     Nullable!TestCaseId getTestCaseId(const TestCase tc) @trusted {
1520         static immutable sql = format!"SELECT id FROM %s WHERE name = :name"(allTestCaseTable);
1521         auto stmt = db.prepare(sql);
1522         stmt.get.bind(":name", tc.name);
1523 
1524         typeof(return) rval;
1525         foreach (res; stmt.get.execute) {
1526             rval = TestCaseId(res.peek!long(0));
1527         }
1528         return rval;
1529     }
1530 
1531     /// The mutation ids are guaranteed to be sorted.
1532     /// Returns: the mutants the test case killed.
1533     MutationId[] getTestCaseMutantKills(const TestCaseId id, const Mutation.Kind[] kinds) @trusted {
1534         immutable sql = format!"SELECT t2.id
1535             FROM %s t1, %s t2
1536             WHERE
1537             t1.tc_id = :tid AND
1538             t1.st_id = t2.st_id AND
1539             t2.kind IN (%(%s,%))
1540             GROUP BY t2.st_id
1541             ORDER BY t2.id"(killedTestCaseTable,
1542                 mutationTable, kinds.map!(a => cast(int) a));
1543 
1544         auto rval = appender!(MutationId[])();
1545         auto stmt = db.prepare(sql);
1546         stmt.get.bind(":tid", cast(long) id);
1547         foreach (a; stmt.get.execute)
1548             rval.put(MutationId(a.peek!long(0)));
1549 
1550         return rval.data;
1551     }
1552 
1553     /// Returns: test cases that killed the mutant.
1554     TestCase[] getTestCases(const MutationId id) @trusted {
1555         Appender!(TestCase[]) rval;
1556 
1557         static immutable get_test_cases_sql = format!"SELECT t1.name,t2.location
1558             FROM %s t1, %s t2, %s t3
1559             WHERE
1560             t3.id = :id AND
1561             t3.st_id = t2.st_id AND
1562             t2.tc_id = t1.id"(
1563                 allTestCaseTable, killedTestCaseTable, mutationTable);
1564         auto stmt = db.prepare(get_test_cases_sql);
1565         stmt.get.bind(":id", cast(long) id);
1566         foreach (a; stmt.get.execute)
1567             rval.put(TestCase(a.peek!string(0), a.peek!string(1)));
1568 
1569         return rval.data;
1570     }
1571 
1572     /// Returns: if the mutant have any test cases recorded that killed it
1573     bool hasTestCases(const MutationStatusId id) @trusted {
1574         static immutable sql = format!"SELECT count(*) FROM %s t0 WHERE t0.st_id = :id"(
1575                 killedTestCaseTable);
1576         auto stmt = db.prepare(sql);
1577         stmt.get.bind(":id", id.get);
1578         foreach (a; stmt.get.execute) {
1579             return a.peek!long(0) != 0;
1580         }
1581         return false;
1582     }
1583 
1584     /** Returns: number of test cases
1585      */
1586     long getNumOfTestCases() @trusted {
1587         static immutable num_test_cases_sql = format!"SELECT count(*) FROM %s"(allTestCaseTable);
1588         return db.execute(num_test_cases_sql).oneValue!long;
1589     }
1590 
1591     /** Returns: test cases that killed other mutants at the same mutation point as `id`.
1592       */
1593     TestCase[] getSurroundingTestCases(const MutationId id) @trusted {
1594         Appender!(TestCase[]) rval;
1595 
1596         // get the mutation point ID that id reside at
1597         long mp_id;
1598         {
1599             auto stmt = db.prepare(format!"SELECT mp_id FROM %s WHERE id=:id"(mutationTable));
1600             stmt.get.bind(":id", cast(long) id);
1601             auto res = stmt.get.execute;
1602             if (res.empty)
1603                 return null;
1604             mp_id = res.oneValue!long;
1605         }
1606 
1607         // get all the test cases that are killed at the mutation point
1608         static immutable get_test_cases_sql = format!"SELECT t2.name,t1.location FROM %s t1,%s t2 WHERE t1.tc_id == t2.id AND t0.st_id IN (SELECT st_id FROM %s WHERE mp_id=:id)"(
1609                 killedTestCaseTable, allTestCaseTable, mutationTable);
1610         auto stmt = db.prepare(get_test_cases_sql);
1611         stmt.get.bind(":id", mp_id);
1612         foreach (a; stmt.get.execute) {
1613             rval.put(TestCase(a.peek!string(0), a.peek!string(1)));
1614         }
1615 
1616         return rval.data;
1617     }
1618 
1619     void removeTestCase(const TestCaseId id) @trusted {
1620         auto stmt = db.prepare(format!"DELETE FROM %s WHERE id=:id"(allTestCaseTable));
1621         stmt.get.bind(":id", cast(long) id);
1622         stmt.get.execute;
1623     }
1624 
1625     /// Change the status of all mutants that the test case has killed to unknown.
1626     void resetTestCaseId(const TestCaseId id) @trusted {
1627         {
1628             static immutable sql = format!"UPDATE %1$s SET status=0 WHERE id IN (SELECT t1.id FROM %2$s t0, %1$s t1 WHERE t0.tc_id = :id AND t0.st_id = t1.id)"(
1629                     mutationStatusTable, killedTestCaseTable);
1630             auto stmt = db.prepare(sql);
1631             stmt.get.bind(":id", cast(long) id);
1632             stmt.get.execute;
1633         }
1634         {
1635             static immutable sql2 = format!"DELETE FROM %1$s WHERE tc_id = :id"(killedTestCaseTable);
1636             auto stmt = db.prepare(sql2);
1637             stmt.get.bind(":id", cast(long) id);
1638             stmt.get.execute;
1639         }
1640     }
1641 
1642     /// Returns: the context for the timeout algorithm.
1643     MutantTimeoutCtx getMutantTimeoutCtx() @trusted {
1644         foreach (res; db.run(select!MutantTimeoutCtx))
1645             return res;
1646         return MutantTimeoutCtx.init;
1647     }
1648 
1649     void putMutantTimeoutCtx(const MutantTimeoutCtx ctx) @trusted {
1650         db.run(delete_!MutantTimeoutCtx);
1651         db.run(insert!MutantTimeoutCtx.insert, ctx);
1652     }
1653 
1654     void putMutantInTimeoutWorklist(const MutationStatusId id) @trusted {
1655         const sql = format!"INSERT OR IGNORE INTO %s (id) VALUES (:id)"(mutantTimeoutWorklistTable);
1656         auto stmt = db.prepare(sql);
1657         stmt.get.bind(":id", id.get);
1658         stmt.get.execute;
1659     }
1660 
1661     /** Remove all mutants that are in the worklist that do NOT have the
1662      * mutation status timeout.
1663      */
1664     void reduceMutantTimeoutWorklist() @trusted {
1665         static immutable sql = format!"DELETE FROM %1$s
1666             WHERE
1667             id IN (SELECT id FROM %2$s WHERE status != :status)"(
1668                 mutantTimeoutWorklistTable, mutationStatusTable);
1669         auto stmt = db.prepare(sql);
1670         stmt.get.bind(":status", cast(ubyte) Mutation.Status.timeout);
1671         stmt.get.execute;
1672     }
1673 
1674     /// Remove all mutants from the worklist.
1675     void clearMutantTimeoutWorklist() @trusted {
1676         static immutable sql = format!"DELETE FROM %1$s"(mutantTimeoutWorklistTable);
1677         db.run(sql);
1678     }
1679 
1680     /// Returns: the number of mutants in the worklist.
1681     long countMutantTimeoutWorklist() @trusted {
1682         static immutable sql = format!"SELECT count(*) FROM %1$s"(mutantTimeoutWorklistTable);
1683         auto stmt = db.prepare(sql);
1684         auto res = stmt.get.execute();
1685         return res.oneValue!long;
1686     }
1687 
1688     /// Changes the status of mutants in the timeout worklist to unknown.
1689     void resetMutantTimeoutWorklist(Mutation.Status toStatus) @trusted {
1690         static immutable sql = format!"UPDATE %1$s SET status=:st WHERE id IN (SELECT id FROM %2$s)"(
1691                 mutationStatusTable, mutantTimeoutWorklistTable);
1692         auto stmt = db.prepare(sql);
1693         stmt.get.bind(":st", cast(ubyte) toStatus);
1694         stmt.get.execute;
1695     }
1696 
1697     /// Copy the timeout mutants to the worklist of mutants to test.
1698     void copyMutantTimeoutWorklist(const long prio = 100) @trusted {
1699         immutable sql = format!"INSERT OR IGNORE INTO %1$s (id,prio)
1700             SELECT id,%3$s FROM %2$s"(mutantWorklistTable,
1701                 mutantTimeoutWorklistTable, prio);
1702         auto stmt = db.prepare(sql);
1703         stmt.get.execute;
1704     }
1705 
1706     /** Update the content of metadata tables with what has been added to the
1707      * raw table data.
1708      */
1709     void updateMetadata() @trusted {
1710         db.run(format!"DELETE FROM %s"(srcMetadataTable));
1711         db.run(format!"DELETE FROM %s"(nomutTable));
1712         db.run(format!"DELETE FROM %s"(nomutDataTable));
1713 
1714         static immutable nomut_tbl = "INSERT INTO %s
1715             SELECT
1716                 t0.id mp_id,
1717                 t1.line line,
1718                 count(*) status
1719                 FROM %s t0, %s t1
1720                 WHERE
1721                 t0.file_id = t1.file_id AND
1722                 (t1.line BETWEEN t0.line AND t0.line_end)
1723                 GROUP BY
1724                 t0.id";
1725         db.run(format!nomut_tbl(nomutTable, mutationPointTable, rawSrcMetadataTable));
1726 
1727         static immutable src_metadata_sql = "INSERT INTO %s
1728             SELECT DISTINCT
1729             t0.id AS mut_id,
1730             t1.id AS st_id,
1731             t2.id AS mp_id,
1732             t3.id AS file_id,
1733             (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut
1734             FROM %s t0, %s t1, %s t2, %s t3
1735             WHERE
1736             t0.mp_id = t2.id AND
1737             t0.st_id = t1.id AND
1738             t2.file_id = t3.id";
1739         db.run(format!src_metadata_sql(srcMetadataTable, nomutTable,
1740                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
1741 
1742         static immutable nomut_data_tbl = "INSERT INTO %s
1743             SELECT
1744                 t0.id as mut_id,
1745                 t0.mp_id as mp_id,
1746                 t1.line as line,
1747                 t1.tag as tag,
1748                 t1.comment as comment
1749                 FROM %s t0, %s t1, %s t2
1750                 WHERE
1751                 t0.mp_id = t2.mp_id AND
1752                 t1.line = t2.line";
1753         db.run(format!nomut_data_tbl(nomutDataTable, mutationTable,
1754                 rawSrcMetadataTable, nomutTable));
1755     }
1756 
1757     /// Returns: all schematas excluding those that are known to not be
1758     /// possible to compile.
1759     SchemataId[] getSchematas() @trusted {
1760         static immutable sql = format!"SELECT t0.id
1761             FROM %1$s t0
1762             WHERE
1763             t0.id NOT IN (SELECT id FROM %2$s)"(
1764                 schemataTable, schemataUsedTable);
1765         auto stmt = db.prepare(sql);
1766         auto app = appender!(SchemataId[])();
1767         foreach (a; stmt.get.execute) {
1768             app.put(SchemataId(a.peek!long(0)));
1769         }
1770         return app.data;
1771     }
1772 
1773     Nullable!Schemata getSchemata(SchemataId id) @trusted {
1774         import std.zlib : uncompress;
1775 
1776         static immutable sql = format!"SELECT
1777             t1.path, t0.text, t0.offset_begin, t0.offset_end
1778             FROM %1$s t0, %2$s t1
1779             WHERE
1780             t0.schem_id = :id AND
1781             t0.file_id = t1.id
1782             ORDER BY t0.order_ ASC
1783             "(schemataFragmentTable, filesTable);
1784 
1785         typeof(return) rval;
1786         auto stmt = db.prepare(sql);
1787         stmt.get.bind(":id", cast(long) id);
1788 
1789         auto app = appender!(SchemataFragment[])();
1790         foreach (a; stmt.get.execute) {
1791             auto raw = a.peek!(ubyte[])(1);
1792             auto offset = Offset(a.peek!uint(2), a.peek!uint(3));
1793             app.put(SchemataFragment(a.peek!string(0).Path, offset,
1794                     cast(const(ubyte)[]) uncompress(raw, offset.end - offset.begin)));
1795         }
1796 
1797         if (!app.data.empty) {
1798             rval = Schemata(SchemataId(id), app.data);
1799         }
1800 
1801         return rval;
1802     }
1803 
1804     /// Returns: number of mutants in a schemata that are marked for testing.
1805     long schemataMutantsCount(const SchemataId id, const Mutation.Kind[] kinds) @trusted {
1806         const sql = format!"SELECT count(*)
1807         FROM %s t1, %s t2, %s t3, %s t4
1808         WHERE
1809         t1.schem_id = :id AND
1810         t1.st_id = t2.id AND
1811         t3.st_id = t1.st_id AND
1812         t2.id = t4.id AND
1813         t3.kind IN (%(%s,%))
1814         "(schemataMutantTable, mutationStatusTable,
1815                 mutationTable, mutantWorklistTable, kinds.map!(a => cast(int) a));
1816 
1817         auto stmt = db.prepare(sql);
1818         stmt.get.bind(":id", id.get);
1819         return stmt.get.execute.oneValue!long;
1820     }
1821 
1822     MutationStatusId[] getSchemataMutants(const SchemataId id, const Mutation.Kind[] kinds) @trusted {
1823         // TODO: DISTINCT should not be needed. Instead use e.g. a constraint on the table or something
1824         immutable sql = format!"SELECT DISTINCT t1.st_id
1825             FROM %s t1, %s t2, %s t3, %s t4
1826             WHERE
1827             t1.schem_id = :id AND
1828             t1.st_id = t2.id AND
1829             t3.st_id = t1.st_id AND
1830             t2.id = t4.id AND
1831             t3.kind IN (%(%s,%))
1832             "(schemataMutantTable, mutationStatusTable,
1833                 mutationTable, mutantWorklistTable, kinds.map!(a => cast(int) a));
1834         auto stmt = db.prepare(sql);
1835         stmt.get.bind(":id", id.get);
1836 
1837         auto app = appender!(MutationStatusId[])();
1838         foreach (a; stmt.get.execute) {
1839             app.put(a.peek!long(0).MutationStatusId);
1840         }
1841 
1842         return app.data;
1843     }
1844 
1845     /// Returns: the kind of mutants a schemata contains.
1846     Mutation.Kind[] getSchemataKinds(const SchemataId id) @trusted {
1847         static immutable sql = format!"SELECT DISTINCT t1.kind
1848             FROM %1$s t0, %2$s t1
1849             WHERE
1850             t0.schem_id = :id AND
1851             t0.st_id = t1.st_id
1852             "(schemataMutantTable, mutationTable);
1853         auto stmt = db.prepare(sql);
1854         stmt.get.bind(":id", cast(long) id);
1855 
1856         auto app = appender!(Mutation.Kind[])();
1857         foreach (a; stmt.get.execute) {
1858             app.put(a.peek!long(0).to!(Mutation.Kind));
1859         }
1860 
1861         return app.data;
1862     }
1863 
1864     /// Mark a schemata as used.
1865     void markUsed(const SchemataId id) @trusted {
1866         static immutable sql = format!"INSERT OR IGNORE INTO %1$s VALUES(:id)"(schemataUsedTable);
1867         auto stmt = db.prepare(sql);
1868         stmt.get.bind(":id", cast(long) id);
1869         stmt.get.execute;
1870     }
1871 
1872     /// Create a schemata from a bundle of fragments.
1873     Nullable!SchemataId putSchemata(SchemataChecksum cs,
1874             const SchemataFragment[] fragments, MutationStatusId[] mutants) @trusted {
1875         import std.range : enumerate;
1876         import std.zlib : compress;
1877 
1878         const schemId = cast(long) cs.value.c0;
1879 
1880         const exists = () {
1881             static immutable sql = format!"SELECT count(*) FROM %1$s WHERE id=:id"(schemataTable);
1882             auto stmt = db.prepare(sql);
1883             stmt.get.bind(":id", schemId);
1884             return stmt.get.execute.oneValue!long != 0;
1885 
1886         }();
1887 
1888         if (exists)
1889             return typeof(return)();
1890 
1891         {
1892             static immutable sql = format!"INSERT INTO %1$s VALUES(:id, :nr)"(schemataTable);
1893             auto stmt = db.prepare(sql);
1894             stmt.get.bind(":id", cast(long) cs.value.c0);
1895             stmt.get.bind(":nr", cast(long) fragments.length);
1896             stmt.get.execute;
1897         }
1898 
1899         foreach (f; fragments.enumerate) {
1900             const fileId = getFileId(f.value.file);
1901             if (fileId.isNull) {
1902                 logger.warningf("Unable to add schemata fragment for file %s because it doesn't exist",
1903                         f.value.file);
1904                 continue;
1905             }
1906 
1907             db.run(insert!SchemataFragmentTable, SchemataFragmentTable(0,
1908                     schemId, cast(long) fileId.get, f.index,
1909                     compress(f.value.text), f.value.offset.begin, f.value.offset.end));
1910         }
1911 
1912         // relate mutants to this schemata.
1913         db.run(insertOrIgnore!SchemataMutantTable,
1914                 mutants.map!(a => SchemataMutantTable(cast(long) a, schemId)));
1915 
1916         return typeof(return)(schemId.SchemataId);
1917     }
1918 
1919     /// Prunes the database of schemas that where created by an older version.
1920     void deleteAllSchemas() @trusted {
1921         db.run(delete_!SchemataTable);
1922     }
1923 
1924     /// Prunes the database of schemas that are unusable.
1925     void pruneSchemas() @trusted {
1926         auto remove = () {
1927             auto remove = appender!(long[])();
1928 
1929             // remove those that have lost some fragments
1930             static immutable sqlFragment = format!"SELECT t0.id
1931             FROM
1932             %1$s t0,
1933             (SELECT schem_id id,count(*) fragments FROM %2$s GROUP BY schem_id) t1
1934             WHERE
1935             t0.id = t1.id AND
1936             t0.fragments != t1.fragments
1937             "(schemataTable,
1938                     schemataFragmentTable);
1939             auto stmt = db.prepare(sqlFragment);
1940             foreach (a; stmt.get.execute) {
1941                 remove.put(a.peek!long(0));
1942             }
1943 
1944             // remove those that have lost all fragments
1945             static immutable sqlNoFragment = format!"SELECT t0.id FROM %1$s t0 WHERE t0.id NOT IN (SELECT schem_id FROM %2$s)"(
1946                     schemataTable, schemataFragmentTable);
1947             stmt = db.prepare(sqlNoFragment);
1948             foreach (a; stmt.get.execute) {
1949                 remove.put(a.peek!long(0));
1950             }
1951 
1952             return remove.data;
1953         }();
1954 
1955         static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(schemataTable);
1956         auto stmt = db.prepare(sql);
1957         foreach (a; remove) {
1958             stmt.get.bind(":id", a);
1959             stmt.get.execute;
1960             stmt.get.reset;
1961         }
1962     }
1963 
1964     /** Removes all schemas that either do not compile or have zero mutants.
1965      *
1966      * Returns: number of schemas removed.
1967      */
1968     long pruneUsedSchemas() @trusted {
1969         auto remove = () {
1970             auto remove = appender!(long[])();
1971 
1972             static immutable sqlUsed = format!"SELECT id FROM %1$s"(schemataUsedTable);
1973             auto stmt = db.prepare(sqlUsed);
1974             foreach (a; stmt.get.execute) {
1975                 remove.put(a.peek!long(0));
1976             }
1977             return remove.data;
1978         }();
1979 
1980         static immutable sql = format!"DELETE FROM %1$s WHERE id=:id"(schemataTable);
1981         auto stmt = db.prepare(sql);
1982         foreach (a; remove) {
1983             stmt.get.bind(":id", a);
1984             stmt.get.execute;
1985             stmt.get.reset;
1986         }
1987 
1988         return remove.length;
1989     }
1990 
1991     /// Compact the database by running a VACUUM operation
1992     void vacuum() @trusted {
1993         db.run("VACUUM");
1994     }
1995 
1996     /// Returns: the stored runtimes in ascending order by their `timeStamp`.
1997     TestCmdRuntime[] getTestCmdRuntimes() @trusted {
1998         import std.algorithm : sort;
1999 
2000         auto app = appender!(TestCmdRuntime[])();
2001         foreach (r; db.run(select!RuntimeHistoryTable)) {
2002             app.put(TestCmdRuntime(r.timeStamp, r.timeMs.dur!"msecs"));
2003         }
2004 
2005         return app.data.sort!((a, b) => a.timeStamp < b.timeStamp).array;
2006     }
2007 
2008     /// Drop all currently stored runtimes and replaces with `runtime`.
2009     void setTestCmdRuntimes(const TestCmdRuntime[] runtimes) @trusted {
2010         import std.range : enumerate;
2011 
2012         db.run(format!"DELETE FROM %s"(runtimeHistoryTable));
2013         db.run(insertOrReplace!RuntimeHistoryTable,
2014                 runtimes.enumerate.map!(a => RuntimeHistoryTable(a.index,
2015                     a.value.timeStamp, a.value.runtime.total!"msecs")));
2016     }
2017 
2018     /// Returns: the stored scores in ascending order by their `time`.
2019     MutationScore[] getMutationScoreHistory() @trusted {
2020         import std.algorithm : sort;
2021 
2022         auto app = appender!(MutationScore[])();
2023         foreach (r; db.run(select!MutationScoreHistoryTable)) {
2024             app.put(MutationScore(r.timeStamp, typeof(MutationScore.score)(r.score)));
2025         }
2026 
2027         return app.data.sort!((a, b) => a.timeStamp < b.timeStamp).array;
2028     }
2029 
2030     /// Add a mutation score to the history table.
2031     void putMutationScore(const MutationScore score) @trusted {
2032         db.run(insert!MutationScoreHistoryTable, MutationScoreHistoryTable(0,
2033                 score.timeStamp, score.score.get));
2034     }
2035 
2036     /// Trim the mutation score history table to only contain the last `keep` scores.
2037     void trimMutationScore(const long keep) @trusted {
2038         auto stmt = db.prepare(format!"SELECT count(*) FROM %s"(mutationScoreHistoryTable));
2039         const sz = stmt.get.execute.oneValue!long;
2040 
2041         if (sz < keep)
2042             return;
2043 
2044         auto ids = appender!(long[])();
2045         stmt = db.prepare(format!"SELECT t0.id FROM t0 %s ORDER BY t0.time ASC LIMIT :limit"(
2046                 mutationScoreHistoryTable));
2047         stmt.get.bind(":limit", sz - keep);
2048         foreach (a; stmt.get.execute)
2049             ids.put(a.peek!long(0));
2050 
2051         stmt = db.prepare(format!"DELETE FROM %s WHERE id = :id"(mutationScoreHistoryTable));
2052         foreach (a; ids.data) {
2053             stmt.get.bind(":id", a);
2054             stmt.get.execute;
2055             stmt.get.reset;
2056         }
2057     }
2058 
2059     /// Add coverage regions.
2060     void putCoverageMap(const FileId id, const Offset[] region) @trusted {
2061         static immutable sql = format!"INSERT OR IGNORE INTO %1$s (file_id, begin, end)
2062             VALUES(:fid, :begin, :end)"(srcCovTable);
2063         auto stmt = db.prepare(sql);
2064 
2065         foreach (a; region) {
2066             stmt.get.bind(":fid", id.get);
2067             stmt.get.bind(":begin", a.begin);
2068             stmt.get.bind(":end", a.end);
2069             stmt.get.execute;
2070             stmt.get.reset;
2071         }
2072     }
2073 
2074     CovRegion[][FileId] getCoverageMap() @trusted {
2075         static immutable sql = format!"SELECT file_id,begin,end,id FROM %s"(srcCovTable);
2076         auto stmt = db.prepare(sql);
2077 
2078         typeof(return) rval;
2079         foreach (ref r; stmt.get.execute) {
2080             auto region = CovRegion(r.peek!long(3).CoverageRegionId,
2081                     Offset(r.peek!uint(1), r.peek!uint(2)));
2082             if (auto v = FileId(r.peek!long(0)) in rval) {
2083                 *v ~= region;
2084             } else {
2085                 rval[FileId(r.peek!long(0))] = [region];
2086             }
2087         }
2088 
2089         return rval;
2090     }
2091 
2092     long getCoverageMapCount() @trusted {
2093         static immutable sql = format!"SELECT count(*) FROM %s"(srcCovTable);
2094         auto stmt = db.prepare(sql);
2095         foreach (ref r; stmt.get.execute)
2096             return r.peek!long(0);
2097         return 0;
2098     }
2099 
2100     void clearCoverageMap(const FileId id) @trusted {
2101         static immutable sql = format!"DELETE FROM %1$s WHERE file_id = :id"(srcCovTable);
2102         auto stmt = db.prepare(sql);
2103         stmt.get.bind(":id", id.get);
2104         stmt.get.execute;
2105     }
2106 
2107     void putCoverageInfo(const CoverageRegionId regionId, bool status) {
2108         static immutable sql = format!"INSERT OR REPLACE INTO %1$s (id, status) VALUES(:id, :status)"(
2109                 srcCovInfoTable);
2110         auto stmt = db.prepare(sql);
2111         stmt.get.bind(":id", regionId.get);
2112         stmt.get.bind(":status", status);
2113         stmt.get.execute;
2114     }
2115 
2116     Optional!SysTime getCoverageTimeStamp() @trusted {
2117         static immutable sql = format!"SELECT timeStamp FROM %s"(srcCovTimeStampTable);
2118         auto stmt = db.prepare(sql);
2119 
2120         foreach (ref r; stmt.get.execute) {
2121             return some(r.peek!string(0).fromSqLiteDateTime);
2122         }
2123         return none!SysTime;
2124     }
2125 
2126     /// Set the timestamp to the current UTC time.
2127     void updateCoverageTimeStamp() @trusted {
2128         static immutable sql = format!"INSERT OR REPLACE INTO %s (id, timestamp) VALUES(0, :time)"(
2129                 srcCovTimeStampTable);
2130         auto stmt = db.prepare(sql);
2131         stmt.get.bind(":time", Clock.currTime.toSqliteDateTime);
2132         stmt.get.execute;
2133     }
2134 
2135     /// Returns: the latest/newest timestamp of the tracked SUT or test files.
2136     Optional!SysTime getLatestTimeStampOfTestOrSut() @trusted {
2137         import std.algorithm : max;
2138 
2139         auto test = getNewestTestFile;
2140         auto sut = getNewestFile;
2141 
2142         if (!(test.hasValue || sut.hasValue))
2143             return none!SysTime;
2144 
2145         return some(max(test.orElse(TestFile.init).timeStamp, sut.orElse(SysTime.init)));
2146     }
2147 
2148     MutationStatusId[] getNotCoveredMutants() @trusted {
2149         static immutable sql = format!"SELECT DISTINCT t3.st_id FROM %1$s t0, %2$s t1, %3$s t2, %4$s t3
2150             WHERE t0.status = 0 AND
2151             t0.id = t1.id AND
2152             t1.file_id = t2.file_id AND
2153             (t2.offset_begin BETWEEN t1.begin AND t1.end) AND
2154             (t2.offset_end BETWEEN t1.begin AND t1.end) AND
2155             t2.id = t3.mp_id"(srcCovInfoTable,
2156                 srcCovTable, mutationPointTable, mutationTable);
2157 
2158         auto app = appender!(MutationStatusId[])();
2159         auto stmt = db.prepare(sql);
2160         foreach (ref r; stmt.get.execute) {
2161             app.put(MutationStatusId(r.peek!long(0)));
2162         }
2163 
2164         return app.data;
2165     }
2166 
2167     DbDependency dependencyApi() return  {
2168         return DbDependency(&this);
2169     }
2170 
2171     DbTestCmd testCmdApi() return  {
2172         return DbTestCmd(&this);
2173     }
2174 }
2175 
2176 /** Dependencies between root and those files that should trigger a re-analyze
2177  * of the root if they are changed.
2178  */
2179 struct DbDependency {
2180     private Database* db;
2181 
2182     /// The root must already exist or the whole operation will fail with an sql error.
2183     void set(const Path path, const DepFile[] deps) @trusted {
2184         static immutable insertDepSql = format!"INSERT OR IGNORE INTO %1$s (file,checksum0,checksum1)
2185             VALUES(:file,:cs0,:cs1)
2186             ON CONFLICT (file) DO UPDATE SET checksum0=:cs0,checksum1=:cs1 WHERE file=:file"(
2187                 depFileTable);
2188 
2189         auto stmt = db.prepare(insertDepSql);
2190         auto ids = appender!(long[])();
2191         foreach (a; deps) {
2192             stmt.get.bind(":file", a.file.toString);
2193             stmt.get.bind(":cs0", cast(long) a.checksum.c0);
2194             stmt.get.bind(":cs1", cast(long) a.checksum.c1);
2195             stmt.get.execute;
2196             stmt.get.reset;
2197 
2198             // can't use lastInsertRowid because a conflict would not update
2199             // the ID.
2200             auto id = getId(a.file);
2201             if (id.hasValue)
2202                 ids.put(id.orElse(0L));
2203         }
2204 
2205         static immutable addRelSql = format!"INSERT OR IGNORE INTO %1$s (dep_id,file_id) VALUES(:did, :fid)"(
2206                 depRootTable);
2207         stmt = db.prepare(addRelSql);
2208         const fid = () {
2209             auto a = db.getFileId(path);
2210             if (a.isNull) {
2211                 throw new Exception(
2212                         "File is not tracked (is missing from the files table in the database) "
2213                         ~ path);
2214             }
2215             return a.get;
2216         }();
2217 
2218         foreach (id; ids.data) {
2219             stmt.get.bind(":did", id);
2220             stmt.get.bind(":fid", fid.get);
2221             stmt.get.execute;
2222             stmt.get.reset;
2223         }
2224     }
2225 
2226     private Optional!long getId(const Path file) {
2227         foreach (a; db.run(select!DependencyFileTable.where("file = :file",
2228                 Bind("file")), file.toString)) {
2229             return some(a.id);
2230         }
2231         return none!long;
2232     }
2233 
2234     /// Returns: all dependencies.
2235     DepFile[] getAll() @trusted {
2236         return db.run(select!DependencyFileTable)
2237             .map!(a => DepFile(Path(a.file), Checksum(a.checksum0, a.checksum1))).array;
2238     }
2239 
2240     /// Returns: all files that a root is dependent on.
2241     Path[] get(const Path root) @trusted {
2242         static immutable sql = format!"SELECT t0.file
2243             FROM %1$s t0, %2$s t1, %3$s t2
2244             WHERE
2245             t0.id = t1.dep_id AND
2246             t1.file_id = t2.id AND
2247             t2.path = :file"(depFileTable,
2248                 depRootTable, filesTable);
2249 
2250         auto stmt = db.prepare(sql);
2251         stmt.get.bind(":file", root.toString);
2252         auto app = appender!(Path[])();
2253         foreach (ref a; stmt.get.execute) {
2254             app.put(Path(a.peek!string(0)));
2255         }
2256 
2257         return app.data;
2258     }
2259 
2260     /// Remove all dependencies that have no relation to a root.
2261     void cleanup() @trusted {
2262         db.run(format!"DELETE FROM %1$s
2263                WHERE id NOT IN (SELECT dep_id FROM %2$s)"(depFileTable,
2264                 depRootTable));
2265     }
2266 
2267 }
2268 
2269 struct DbTestCmd {
2270     import my.hash : Checksum64;
2271 
2272     private Database* db;
2273 
2274     void set(string testCmd, ChecksumTestCmdOriginal cs) @trusted {
2275         static immutable sql = format!"INSERT OR IGNORE INTO %1$s (checksum, cmd) VALUES(:cs, :cmd)"(
2276                 testCmdOriginalTable);
2277 
2278         auto stmt = db.prepare(sql);
2279         stmt.get.bind(":cs", cast(long) cs.get.c0);
2280         stmt.get.bind(":cmd", testCmd);
2281         stmt.get.execute;
2282     }
2283 
2284     void removeOriginal(string testCmd) @trusted {
2285         static immutable sql = "DELETE FROM " ~ testCmdOriginalTable ~ " WHERE cmd = :cmd";
2286         auto stmt = db.prepare(sql);
2287         stmt.get.bind(":cmd", testCmd);
2288         stmt.get.execute;
2289     }
2290 
2291     void remove(ChecksumTestCmdOriginal cs) @trusted {
2292         static immutable sql = "DELETE FROM " ~ testCmdOriginalTable ~ " WHERE checksum = :cs";
2293         auto stmt = db.prepare(sql);
2294         stmt.get.bind(":cs", cast(long) cs.get.c0);
2295         stmt.get.execute;
2296     }
2297 
2298     Set!Checksum64 original() @trusted {
2299         static immutable sql = "SELECT checksum FROM " ~ testCmdOriginalTable;
2300 
2301         auto stmt = db.prepare(sql);
2302         typeof(return) rval;
2303         foreach (ref r; stmt.get.execute)
2304             rval.add(Checksum64(cast(ulong) r.peek!long(0)));
2305         return rval;
2306     }
2307 
2308     void add(ChecksumTestCmdMutated cs, Mutation.Status status) @trusted {
2309         static immutable sql = format!"INSERT OR REPLACE INTO %1$s (checksum,status,timestamp) VALUES(:cs,:status,:ts)"(
2310                 testCmdMutatedTable);
2311 
2312         auto stmt = db.prepare(sql);
2313         stmt.get.bind(":cs", cast(long) cs.get.c0);
2314         stmt.get.bind(":status", cast(long) status);
2315         stmt.get.bind(":ts", Clock.currTime.toSqliteDateTime);
2316         stmt.get.execute;
2317     }
2318 
2319     /// Trim the saved checksums to only the latest `keep`.
2320     void trimMutated(const long keep) @trusted {
2321         auto stmt = db.prepare(format!"SELECT count(*) FROM %s"(testCmdMutatedTable));
2322         const sz = stmt.get.execute.oneValue!long;
2323         if (sz < keep)
2324             return;
2325 
2326         auto ids = appender!(long[])();
2327         stmt = db.prepare(format!"SELECT checksum FROM %s ORDER BY timestamp ASC LIMIT :limit"(
2328                 testCmdMutatedTable));
2329         stmt.get.bind(":limit", sz - keep);
2330         foreach (a; stmt.get.execute)
2331             ids.put(a.peek!long(0));
2332 
2333         stmt = db.prepare(format!"DELETE FROM %s WHERE checksum = :cs"(testCmdMutatedTable));
2334         foreach (a; ids.data) {
2335             stmt.get.bind(":cs", a);
2336             stmt.get.execute;
2337             stmt.get.reset;
2338         }
2339     }
2340 
2341     Mutation.Status[Checksum64] mutated() @trusted {
2342         static immutable sql = "SELECT checksum,status FROM " ~ testCmdMutatedTable;
2343 
2344         auto stmt = db.prepare(sql);
2345         typeof(return) rval;
2346         foreach (ref r; stmt.get.execute)
2347             rval[Checksum64(cast(ulong) r.peek!long(0))] = r.peek!long(1).to!(Mutation.Status);
2348         return rval;
2349     }
2350 }
2351 
2352 private:
2353 
2354 MarkedMutant make(MarkedMutantTbl m) {
2355     import dextool.plugin.mutate.backend.type;
2356 
2357     return MarkedMutant(m.mutationStatusId.MutationStatusId, Checksum(m.checksum0,
2358             m.checksum1), m.mutationId.MutationId, SourceLoc(m.line, m.column),
2359             m.path.Path, m.toStatus.to!(Mutation.Status), m.time,
2360             m.rationale.Rationale, m.mutText);
2361 }
2362 
2363 string fromOrder(const MutationOrder userOrder) {
2364     final switch (userOrder) {
2365     case MutationOrder.random:
2366         return ":base_prio + t1.prio + abs(random() % 100)";
2367     case MutationOrder.consecutive:
2368         return ":base_prio";
2369     case MutationOrder.bySize:
2370         return ":base_prio + t1.prio";
2371     }
2372 }