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