1 /**
2 Copyright: Copyright (c) 2018, 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;
25 import std.array : Appender, appender, array;
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 miniorm : toSqliteDateTime, fromSqLiteDateTime;
35 
36 import dextool.type : AbsolutePath, Path, ExitStatusType;
37 
38 import dextool.plugin.mutate.backend.database.schema;
39 import dextool.plugin.mutate.backend.database.type;
40 import dextool.plugin.mutate.backend.type : Language, Checksum;
41 
42 /** Database wrapper with minimal dependencies.
43  */
44 struct Database {
45     import miniorm : Miniorm, select, insert, insertOrReplace, delete_;
46     import d2sqlite3 : SqlDatabase = Database;
47     import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum;
48 
49     Miniorm db;
50     alias db this;
51 
52     /** Create a database by either opening an existing or initializing a new.
53      *
54      * Params:
55      *  db = path to the database
56      */
57     static auto make(string db) @safe {
58         return Database(initializeDB(db));
59     }
60 
61     /// If the file has already been analyzed.
62     bool isAnalyzed(const Path p) @trusted {
63         auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1");
64         stmt.get.bind(":path", cast(string) p);
65         auto res = stmt.get.execute;
66         return res.oneValue!long != 0;
67     }
68 
69     /// If the file has already been analyzed.
70     bool isAnalyzed(const Path p, const Checksum cs) @trusted {
71         auto stmt = db.prepare(
72                 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1");
73         stmt.get.bind(":path", cast(string) p);
74         stmt.get.bind(":cs0", cast(long) cs.c0);
75         stmt.get.bind(":cs1", cast(long) cs.c1);
76         auto res = stmt.get.execute;
77         return res.oneValue!long != 0;
78     }
79 
80     bool exists(MutationStatusId status_id) {
81         immutable s = format!"SELECT COUNT(*) FROM %s WHERE id=:id LIMIT 1"(mutationStatusTable);
82         auto stmt = db.prepare(s);
83         stmt.get.bind(":id", cast(long) status_id);
84         auto res = stmt.get.execute;
85         return res.oneValue!long == 0;
86     }
87 
88     bool isMarked(MutationId id) @trusted {
89         immutable s = format!"SELECT COUNT(*) FROM %s WHERE st_id IN
90             (SELECT st_id FROM %s WHERE id=:id)"(
91                 markedMutantTable, mutationTable);
92         auto stmt = db.prepare(s);
93         stmt.get.bind(":id", cast(long) id);
94         auto res = stmt.get.execute;
95         return res.oneValue!long != 0;
96     }
97 
98     /// All marked mutants whom have a mutation status checksum that has been removed from the database.
99     MarkedMutant[] getLostMarkings() @trusted {
100         immutable sql = format!"SELECT checksum0 FROM %s
101             WHERE
102             checksum0 NOT IN (SELECT checksum0 FROM %s)"(
103                 markedMutantTable, mutationStatusTable);
104 
105         auto stmt = db.prepare(sql);
106         auto app = appender!(MarkedMutant[])();
107         foreach (res; stmt.get.execute) {
108             foreach (m; db.run(select!MarkedMutantTbl.where("checksum0 = ", res.peek!long(0)))) {
109                 app.put(.make(m));
110             }
111         }
112 
113         return app.data;
114     }
115 
116     Nullable!FileId getFileId(const Path p) @trusted {
117         enum sql = format("SELECT id FROM %s WHERE path=:path", filesTable);
118         auto stmt = db.prepare(sql);
119         stmt.get.bind(":path", cast(string) p);
120         auto res = stmt.get.execute;
121 
122         typeof(return) rval;
123         if (!res.empty)
124             rval = FileId(res.oneValue!long);
125         return rval;
126     }
127 
128     /// Returns: the path ID for the mutant.
129     Nullable!FileId getFileId(const MutationId id) @trusted {
130         enum sql = format("SELECT t1.file_id
131             FROM %s t0, %s t1
132             WHERE t0.id = :id AND t0.mp_id = t1.id",
133                     mutationTable, mutationPointTable);
134         auto stmt = db.prepare(sql);
135         stmt.get.bind(":id", cast(long) id);
136 
137         typeof(return) rval;
138         foreach (r; stmt.get.execute)
139             rval = FileId(r.peek!long(0));
140         return rval;
141     }
142 
143     /// Returns: the file path that the id correspond to.
144     Nullable!Path getFile(const FileId id) @trusted {
145         enum sql = format("SELECT path FROM %s WHERE id = :id", filesTable);
146         auto stmt = db.prepare(sql);
147         stmt.get.bind(":id", cast(long) id);
148 
149         typeof(return) rval;
150         foreach (r; stmt.get.execute)
151             rval = Path(r.peek!string(0));
152         return rval;
153     }
154 
155     /// Remove the file with all mutations that are coupled to it.
156     void removeFile(const Path p) @trusted {
157         auto stmt = db.prepare(format!"DELETE FROM %s WHERE path=:path"(filesTable));
158         stmt.get.bind(":path", cast(string) p);
159         stmt.get.execute;
160     }
161 
162     /// Returns: All files in the database as relative paths.
163     Path[] getFiles() @trusted {
164         auto stmt = db.prepare(format!"SELECT path from %s"(filesTable));
165         auto res = stmt.get.execute;
166 
167         auto app = appender!(Path[]);
168         foreach (ref r; res) {
169             app.put(Path(r.peek!string(0)));
170         }
171 
172         return app.data;
173     }
174 
175     enum CntAction {
176         /// Increment the counter
177         incr,
178         /// Reset the counter to zero
179         reset,
180     }
181 
182     /** Update the status of a mutant.
183      *
184      * Params:
185      *  id = ID of the mutant
186      *  st = status to broadcast
187      *  d = time spent on veryfing the mutant
188      *  tcs = test cases that killed the mutant
189      *  counter = how to act with the counter
190      */
191     void updateMutation(const MutationId id, const Mutation.Status st,
192             const Duration d, const(TestCase)[] tcs, CntAction counter = CntAction.incr) @trusted {
193         enum sql = "UPDATE %s SET
194             status=:st,time=:time,update_ts=:update_ts,%s
195             WHERE
196             id IN (SELECT st_id FROM %s WHERE id = :id)";
197 
198         auto stmt = () {
199             final switch (counter) {
200             case CntAction.incr:
201                 return db.prepare(format!sql(mutationStatusTable,
202                         "test_cnt=test_cnt+1", mutationTable));
203             case CntAction.reset:
204                 return db.prepare(format!sql(mutationStatusTable,
205                         "test_cnt=0", mutationTable));
206             }
207         }();
208 
209         stmt.get.bind(":st", st.to!long);
210         stmt.get.bind(":id", id.to!long);
211         stmt.get.bind(":time", d.total!"msecs");
212         stmt.get.bind(":update_ts", Clock.currTime.toUTC.toSqliteDateTime);
213         stmt.get.execute;
214 
215         updateMutationTestCases(id, tcs);
216     }
217 
218     /** Update the counter of how many times the mutants has been alive.
219      *
220      * Params:
221      *  id = ID of the mutant
222      *  counter = how to act with the counter
223      */
224     void updateMutation(const MutationStatusId id, const CntAction counter) @trusted {
225         enum sql = "UPDATE %s SET %s WHERE id = :id";
226 
227         auto stmt = () {
228             final switch (counter) {
229             case CntAction.incr:
230                 return db.prepare(format!sql(mutationStatusTable,
231                         "test_cnt=test_cnt+1"));
232             case CntAction.reset:
233                 return db.prepare(format!sql(mutationStatusTable, "test_cnt=0"));
234             }
235         }();
236 
237         stmt.get.bind(":id", id.to!long);
238         stmt.get.execute;
239     }
240 
241     /// Update the time used to test the mutant.
242     void updateMutation(const MutationStatusId id, const Duration testTime) @trusted {
243         enum sql = format!"UPDATE %s SET time=:time WHERE id = :id"(mutationStatusTable);
244         auto stmt = db.prepare(sql);
245         stmt.get.bind(":id", id.to!long);
246         stmt.get.bind(":time", testTime.total!"msecs");
247         stmt.get.execute;
248     }
249 
250     /** Update the status of a mutant.
251      *
252      * Params:
253      *  id = mutation status ID
254      *  st = new status
255      *  update_ts = if the update timestamp should be updated.
256      */
257     void updateMutationStatus(const MutationStatusId id, const Mutation.Status st,
258             Flag!"updateTs" update_ts = No.updateTs) @trusted {
259 
260         auto stmt = () {
261             if (update_ts) {
262                 const ts = Clock.currTime.toUTC.toSqliteDateTime;
263                 auto s = db.prepare(format!"UPDATE %s SET status=:st,update_ts=:update_ts WHERE id=:id"(
264                         mutationStatusTable));
265                 s.get.bind(":update_ts", ts);
266                 return s;
267             } else
268                 return db.prepare(format!"UPDATE %s SET status=:st WHERE id=:id"(
269                         mutationStatusTable));
270         }();
271         stmt.get.bind(":st", st.to!long);
272         stmt.get.bind(":id", id.to!long);
273         stmt.get.execute;
274     }
275 
276     /// Returns: all mutation status IDs.
277     MutationStatusId[] getAllMutationStatus() @trusted {
278         enum sql = format!"SELECT id FROM %s"(mutationStatusTable);
279 
280         auto app = appender!(MutationStatusId[])();
281         auto stmt = db.prepare(sql);
282         foreach (r; stmt.get.execute)
283             app.put(MutationStatusId(r.peek!long(0)));
284         return app.data;
285     }
286 
287     Nullable!(Mutation.Status) getMutationStatus(const MutationStatusId id) @trusted {
288         enum sql = format!"SELECT status FROM %s WHERE id=:id"(mutationStatusTable);
289         auto stmt = db.prepare(sql);
290         stmt.get.bind(":id", cast(long) id);
291 
292         typeof(return) rval;
293         foreach (a; stmt.get.execute) {
294             rval = cast(Mutation.Status) a.peek!long(0);
295         }
296         return rval;
297     }
298 
299     Nullable!MutationEntry getMutation(const MutationId id) @trusted {
300         import dextool.plugin.mutate.backend.type;
301         import dextool.type : FileName;
302 
303         typeof(return) rval;
304 
305         enum get_mut_sql = format("SELECT
306             t0.id,
307             t0.kind,
308             t3.time,
309             t1.offset_begin,
310             t1.offset_end,
311             t1.line,
312             t1.column,
313             t2.path,
314             t2.lang
315             FROM %s t0,%s t1,%s t2,%s t3
316             WHERE
317             t0.id == :id AND
318             t0.mp_id == t1.id AND
319             t1.file_id == t2.id AND
320             t3.id = t0.st_id
321             ", mutationTable, mutationPointTable,
322                     filesTable, mutationStatusTable);
323 
324         auto stmt = db.prepare(get_mut_sql);
325         stmt.get.bind(":id", cast(long) id);
326         auto res = stmt.get.execute;
327 
328         if (res.empty)
329             return rval;
330 
331         auto v = res.front;
332 
333         auto mp = MutationPoint(Offset(v.peek!uint(3), v.peek!uint(4)));
334         mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))];
335         auto pkey = MutationId(v.peek!long(0));
336         auto file = Path(FileName(v.peek!string(7)));
337         auto sloc = SourceLoc(v.peek!uint(5), v.peek!uint(6));
338         auto lang = v.peek!long(8).to!Language;
339 
340         rval = MutationEntry(pkey, file, sloc, mp, v.peek!long(2).dur!"msecs", lang);
341 
342         return rval;
343     }
344 
345     MutantMetaData getMutantationMetaData(const MutationId id) @trusted {
346         auto rval = MutantMetaData(id);
347         foreach (res; db.run(select!NomutDataTbl.where("mut_id =", cast(long) id))) {
348             rval.set(NoMut(res.tag, res.comment));
349         }
350         return rval;
351     }
352 
353     //TODO: this is a bit inefficient. it should use a callback iterator
354     MutantMetaData[] getMutantationMetaData(const Mutation.Kind[] kinds, const Mutation
355             .Status status) @trusted {
356         const sql = format!"SELECT DISTINCT t.mut_id, t.tag, t.comment
357         FROM %s t, %s t1, %s t2
358         WHERE
359         t.mut_id = t1.id AND
360         t1.st_id = t2.id AND
361         t2.status = :status AND
362         t1.kind IN (%(%s,%))
363         ORDER BY
364         t.mut_id"(nomutDataTable, mutationTable,
365                 mutationStatusTable, kinds.map!(a => cast(long) a));
366         auto stmt = db.prepare(sql);
367         stmt.get.bind(":status", cast(long) status);
368 
369         auto app = appender!(MutantMetaData[])();
370         foreach (res; stmt.get.execute) {
371             app.put(MutantMetaData(MutationId(res.peek!long(0)),
372                     MutantAttr(NoMut(res.peek!string(1), res.peek!string(2)))));
373         }
374         return app.data;
375     }
376 
377     Nullable!Path getPath(const MutationId id) @trusted {
378         enum get_path_sql = format("SELECT t2.path
379             FROM
380             %s t0, %s t1, %s t2
381             WHERE
382             t0.id = :id AND
383             t0.mp_id = t1.id AND
384             t1.file_id = t2.id
385             ", mutationTable, mutationPointTable, filesTable);
386 
387         auto stmt = db.prepare(get_path_sql);
388         stmt.get.bind(":id", cast(long) id);
389         auto res = stmt.get.execute;
390 
391         typeof(return) rval;
392         if (!res.empty)
393             rval = Path(res.front.peek!string(0));
394         return rval;
395     }
396 
397     /// Returns: the mutants that are connected to the mutation statuses.
398     MutantInfo[] getMutantsInfo(const Mutation.Kind[] kinds, const(MutationStatusId)[] id) @trusted {
399         const get_mutid_sql = format("SELECT t0.id,t2.status,t0.kind,t1.line,t1.column
400             FROM %s t0,%s t1, %s t2
401             WHERE
402             t0.st_id IN (%(%s,%)) AND
403             t0.st_id = t2.id AND
404             t0.kind IN (%(%s,%)) AND
405             t0.mp_id = t1.id",
406                 mutationTable, mutationPointTable,
407                 mutationStatusTable, id.map!(a => cast(long) a), kinds.map!(a => cast(int) a));
408         auto stmt = db.prepare(get_mutid_sql);
409 
410         auto app = appender!(MutantInfo[])();
411         foreach (res; stmt.get.execute)
412             app.put(MutantInfo(MutationId(res.peek!long(0)), res.peek!long(1)
413                     .to!(Mutation.Status), res.peek!long(2).to!(Mutation.Kind),
414                     SourceLoc(res.peek!uint(3), res.peek!uint(4))));
415 
416         return app.data;
417     }
418 
419     /// Returns: the mutants that are connected to the mutation statuses.
420     MutationId[] getMutationIds(const(Mutation.Kind)[] kinds, const(MutationStatusId)[] id) @trusted {
421         if (id.length == 0)
422             return null;
423 
424         const get_mutid_sql = format!"SELECT id FROM %s t0
425             WHERE
426             t0.st_id IN (%(%s,%)) AND
427             t0.kind IN (%(%s,%))"(mutationTable,
428                 id.map!(a => cast(long) a), kinds.map!(a => cast(int) a));
429         auto stmt = db.prepare(get_mutid_sql);
430 
431         auto app = appender!(MutationId[])();
432         foreach (res; stmt.get.execute)
433             app.put(MutationId(res.peek!long(0)));
434         return app.data;
435     }
436 
437     Nullable!MutationId getMutationId(const MutationStatusId id) @trusted {
438         immutable sql = format!"SELECT id FROM %s WHERE st_id=:st_id LIMIT=1"(mutationTable);
439         auto stmt = db.prepare(sql);
440         stmt.get.bind(":st_id", cast(long) id);
441 
442         typeof(return) rval;
443         foreach (res; stmt.get.execute) {
444             rval = res.peek!long(0).MutationId;
445         }
446         return rval;
447     }
448 
449     Nullable!MutationStatusId getMutationStatusId(const MutationId id) @trusted {
450         immutable sql = format!"SELECT st_id FROM %s WHERE id=:id"(mutationTable);
451         auto stmt = db.prepare(sql);
452         stmt.get.bind(":id", cast(long) id);
453 
454         typeof(return) rval;
455         foreach (res; stmt.get.execute) {
456             rval = MutationStatusId(res.peek!long(0));
457         }
458         return rval;
459     }
460 
461     Nullable!MutationStatusId getMutationStatusId(const Checksum cs) @trusted {
462         immutable sql = format!"SELECT st_id FROM %s WHERE checksum0=:cs0 AND checksum1=:cs1"(
463                 mutationStatusTable);
464         auto stmt = db.prepare(sql);
465         stmt.get.bind(":cs0", cast(long) cs.c0);
466         stmt.get.bind(":cs1", cast(long) cs.c1);
467 
468         typeof(return) rval;
469         foreach (res; stmt.get.execute) {
470             rval = MutationStatusId(res.peek!long(0));
471         }
472         return rval;
473     }
474 
475     // Returns: the status of the mutant
476     Nullable!(Mutation.Status) getMutationStatus(const MutationId id) @trusted {
477         auto s = format!"SELECT status FROM %s WHERE id IN (SELECT st_id FROM %s WHERE id=:mut_id)"(
478                 mutationStatusTable, mutationTable);
479         auto stmt = db.prepare(s);
480         stmt.get.bind(":mut_id", cast(long) id);
481         typeof(return) rval;
482         foreach (res; stmt.get.execute)
483             rval = res.peek!long(0).to!(Mutation.Status);
484         return rval;
485     }
486 
487     /// Returns: the mutants in the file at the line.
488     MutationStatusId[] getMutationsOnLine(const(Mutation.Kind)[] kinds, FileId fid, SourceLoc sloc) @trusted {
489         // TODO: should it also be line_end?
490         const sql = format("SELECT DISTINCT t0.id FROM %s t0, %s t1, %s t2
491                     WHERE
492                     t1.st_id = t0.id AND
493                     t1.kind IN (%(%s,%)) AND
494                     t1.mp_id = t2.id AND
495                     t2.file_id = :fid AND
496                     (:line BETWEEN t2.line AND t2.line_end)
497                     ",
498                 mutationStatusTable, mutationTable, mutationPointTable,
499                 kinds.map!(a => cast(int) a));
500         auto stmt = db.prepare(sql);
501         stmt.get.bind(":fid", cast(long) fid);
502         stmt.get.bind(":line", sloc.line);
503 
504         auto app = appender!(typeof(return))();
505         foreach (res; stmt.get.execute)
506             app.put(MutationStatusId(res.peek!long(0)));
507         return app.data;
508     }
509 
510     LineMetadata getLineMetadata(const FileId fid, const SourceLoc sloc) @trusted {
511         // TODO: change this select to using microrm
512         enum sql = format("SELECT nomut,tag,comment FROM %s
513             WHERE
514             file_id = :fid AND
515             line = :line", rawSrcMetadataTable);
516         auto stmt = db.prepare(sql);
517         stmt.get.bind(":fid", cast(long) fid);
518         stmt.get.bind(":line", sloc.line);
519 
520         auto rval = typeof(return)(fid, sloc.line);
521         foreach (res; stmt.get.execute) {
522             if (res.peek!long(0) != 0)
523                 rval.set(NoMut(res.peek!string(1), res.peek!string(2)));
524         }
525 
526         return rval;
527     }
528 
529     /// Returns: the `nr` mutants that where the longst since they where tested.
530     MutationStatusTime[] getOldestMutants(const(Mutation.Kind)[] kinds, const long nr) @trusted {
531         const sql = format("SELECT t0.id,t0.update_ts FROM %s t0, %s t1
532                     WHERE
533                     t0.update_ts IS NOT NULL AND
534                     t1.st_id = t0.id AND
535                     t1.kind IN (%(%s,%))
536                     ORDER BY t0.update_ts ASC LIMIT :limit",
537                 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a));
538         auto stmt = db.prepare(sql);
539         stmt.get.bind(":limit", nr);
540 
541         auto app = appender!(MutationStatusTime[])();
542         foreach (res; stmt.get.execute)
543             app.put(MutationStatusTime(MutationStatusId(res.peek!long(0)),
544                     res.peek!string(1).fromSqLiteDateTime));
545         return app.data;
546     }
547 
548     /// Returns: the `nr` mutant with the highest count that has not been killed and existed in the system the longest.
549     MutationStatus[] getHardestToKillMutant(const(Mutation.Kind)[] kinds,
550             const Mutation.Status status, const long nr) @trusted {
551         const sql = format("SELECT t0.id,t0.status,t0.test_cnt,t0.update_ts,t0.added_ts
552             FROM %s t0, %s t1
553             WHERE
554             t0.update_ts IS NOT NULL AND
555             t0.status = :status AND
556             t1.st_id = t0.id AND
557             t1.kind IN (%(%s,%)) AND
558             t1.st_id NOT IN (SELECT st_id FROM %s WHERE nomut != 0)
559             ORDER BY
560             t0.test_cnt DESC,
561             t0.added_ts ASC,
562             t0.update_ts ASC
563             LIMIT :limit",
564                 mutationStatusTable, mutationTable, kinds.map!(a => cast(int) a), srcMetadataTable);
565         auto stmt = db.prepare(sql);
566         stmt.get.bind(":status", cast(long) status);
567         stmt.get.bind(":limit", nr);
568 
569         auto app = appender!(MutationStatus[])();
570         foreach (res; stmt.get.execute) {
571             auto added = () {
572                 auto raw = res.peek!string(4);
573                 if (raw.length == 0)
574                     return Nullable!SysTime();
575                 return Nullable!SysTime(raw.fromSqLiteDateTime);
576             }();
577 
578             // dfmt off
579             app.put(MutationStatus(
580                 MutationStatusId(res.peek!long(0)),
581                 res.peek!long(1).to!(Mutation.Status),
582                 res.peek!long(2).MutantTestCount,
583                 res.peek!string(3).fromSqLiteDateTime,
584                 added,
585             ));
586             // dfmt on
587         }
588 
589         return app.data;
590     }
591 
592     /** Get SourceLoc for a specific mutation id.
593      */
594     Nullable!SourceLoc getSourceLocation(MutationId id) @trusted {
595         auto s = format!"SELECT line, column FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE id=:mut_id)"(
596                 mutationPointTable, mutationTable);
597         auto stmt = db.prepare(s);
598         stmt.get.bind(":mut_id", cast(long) id);
599         typeof(return) rval;
600         foreach (res; stmt.get.execute)
601             rval = SourceLoc(res.peek!uint(0), res.peek!uint(1));
602         return rval;
603     }
604 
605     /** Remove all mutations of kinds.
606      */
607     void removeMutant(const Mutation.Kind[] kinds) @trusted {
608         const s = format!"DELETE FROM %s WHERE id IN (SELECT mp_id FROM %s WHERE kind IN (%(%s,%)))"(
609                 mutationPointTable, mutationTable, kinds.map!(a => cast(int) a));
610         auto stmt = db.prepare(s);
611         stmt.get.execute;
612     }
613 
614     /** Reset all mutations of kinds with the status `st` to unknown.
615      */
616     void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted {
617         const s = format!"UPDATE %s SET status=%s WHERE status = %s AND id IN(SELECT st_id FROM %s WHERE kind IN (%(%s,%)))"(
618                 mutationStatusTable, to_st.to!long, st.to!long,
619                 mutationTable, kinds.map!(a => cast(int) a));
620         auto stmt = db.prepare(s);
621         stmt.get.execute;
622     }
623 
624     /** Mark a mutant with status and rationale (also adds metadata).
625      */
626     void markMutant(const MutationId id, const Path file, const SourceLoc sloc,
627             const MutationStatusId st_id, const Checksum cs,
628             const Mutation.Status s, const Rationale r, string mutationTxt) @trusted {
629         db.run(insertOrReplace!MarkedMutantTbl, MarkedMutantTbl(cs.c0, cs.c1,
630                 st_id, id, sloc.line, sloc.column, file, s, Clock.currTime.toUTC, r, mutationTxt));
631     }
632 
633     void removeMarkedMutant(const Checksum cs) @trusted {
634         db.run(delete_!MarkedMutantTbl.where("checksum0 = ", cast(long) cs.c0));
635     }
636 
637     void removeMarkedMutant(const MutationStatusId id) @trusted {
638         db.run(delete_!MarkedMutantTbl.where("st_id = ", id.to!string));
639     }
640 
641     /// Returns: All mutants with that are marked orderd by their path
642     MarkedMutant[] getMarkedMutants() @trusted {
643         import miniorm : OrderingTermSort;
644 
645         auto app = appender!(MarkedMutant[])();
646         foreach (m; db.run(select!MarkedMutantTbl.orderBy(OrderingTermSort.ASC, [
647                     "path"
648                 ]))) {
649             app.put(.make(m));
650         }
651 
652         return app.data;
653     }
654 
655     Mutation.Kind getKind(MutationId id) @trusted {
656         immutable s = format!"SELECT kind FROM %s WHERE id=:id"(mutationTable);
657         auto stmt = db.prepare(s);
658         stmt.get.bind(":id", cast(long) id);
659 
660         typeof(return) rval;
661         foreach (res; stmt.get.execute) {
662             rval = res.peek!long(0).to!(Mutation.Kind);
663         }
664         return rval;
665     }
666 
667     import dextool.plugin.mutate.backend.type;
668 
669     alias aliveMutants = countMutants!([Mutation.Status.alive], false);
670     alias killedMutants = countMutants!([Mutation.Status.killed], false);
671     alias timeoutMutants = countMutants!([Mutation.Status.timeout], false);
672 
673     /// Returns: Total that should be counted when calculating the mutation score.
674     alias totalMutants = countMutants!([
675             Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout
676             ], false);
677 
678     alias unknownMutants = countMutants!([Mutation.Status.unknown], false);
679     alias killedByCompilerMutants = countMutants!([
680             Mutation.Status.killedByCompiler
681             ], false);
682 
683     alias aliveSrcMutants = countMutants!([Mutation.Status.alive], true);
684     alias killedSrcMutants = countMutants!([Mutation.Status.killed], true);
685     alias timeoutSrcMutants = countMutants!([Mutation.Status.timeout], true);
686 
687     /// Returns: Total that should be counted when calculating the mutation score.
688     alias totalSrcMutants = countMutants!([
689             Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout
690             ], true);
691 
692     alias unknownSrcMutants = countMutants!([Mutation.Status.unknown], true);
693     alias killedByCompilerSrcMutants = countMutants!([
694             Mutation.Status.killedByCompiler
695             ], true);
696 
697     /** Count the mutants with the nomut metadata.
698      *
699      * Params:
700      *  status = status the mutants must be in to be counted.
701      *  distinc = count based on unique source code changes.
702      *  kinds = the kind of mutants to count.
703      *  file = file to count mutants in.
704      */
705     private MutationReportEntry countMutants(int[] status, bool distinct)(
706             const Mutation.Kind[] kinds, string file = null) @trusted {
707         static if (distinct) {
708             auto qq = "
709                 SELECT count(*),sum(time)
710                 FROM (
711                 SELECT count(*),sum(t1.time) time
712                 FROM %s t0, %s t1%s
713                 WHERE
714                 %s
715                 t0.st_id = t1.id AND
716                 t1.status IN (%(%s,%)) AND
717                 t0.kind IN (%(%s,%))
718                 GROUP BY t1.id)";
719         } else {
720             auto qq = "
721                 SELECT count(*),sum(t1.time) time
722                 FROM %s t0, %s t1%s
723                 WHERE
724                 %s
725                 t0.st_id = t1.id AND
726                 t1.status IN (%(%s,%)) AND
727                 t0.kind IN (%(%s,%))";
728         }
729         const query = () {
730             auto fq = file.length == 0
731                 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND";
732             auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3",
733                     mutationPointTable, filesTable);
734             return format(qq, mutationTable, mutationStatusTable, fq_from, fq,
735                     status, kinds.map!(a => cast(int) a));
736         }();
737 
738         typeof(return) rval;
739         auto stmt = db.prepare(query);
740         if (file.length != 0)
741             stmt.get.bind(":path", file);
742         auto res = stmt.get.execute;
743         if (!res.empty)
744             rval = MutationReportEntry(res.front.peek!long(0),
745                     res.front.peek!long(1).dur!"msecs");
746         return rval;
747     }
748 
749     /** Count the mutants with the nomut metadata.
750      *
751      * Params:
752      *  status = status the mutants must be in to be counted.
753      *  distinc = count based on unique source code changes.
754      *  kinds = the kind of mutants to count.
755      *  file = file to count mutants in.
756      */
757     private MetadataNoMutEntry countNoMutMutants(int[] status, bool distinct)(
758             const Mutation.Kind[] kinds, string file = null) @trusted {
759         static if (distinct) {
760             auto sql_base = "
761                 SELECT count(*)
762                 FROM (
763                 SELECT count(*)
764                 FROM %s t0, %s t1,%s t4%s
765                 WHERE
766                 %s
767                 t0.st_id = t1.id AND
768                 t0.st_id = t4.st_id AND
769                 t4.nomut != 0 AND
770                 t1.status IN (%(%s,%)) AND
771                 t0.kind IN (%(%s,%))
772                 GROUP BY t1.id)";
773         } else {
774             auto sql_base = "
775                 SELECT count(*)
776                 FROM %s t0, %s t1,%s t4%s
777                 WHERE
778                 %s
779                 t0.st_id = t1.id AND
780                 t0.st_id = t4.st_id AND
781                 t4.nomut != 0 AND
782                 t1.status IN (%(%s,%)) AND
783                 t0.kind IN (%(%s,%))";
784         }
785         const query = () {
786             auto fq = file.length == 0
787                 ? null : "t0.mp_id = t2.id AND t2.file_id = t3.id AND t3.path = :path AND";
788             auto fq_from = file.length == 0 ? null : format(", %s t2, %s t3",
789                     mutationPointTable, filesTable);
790             return format(sql_base, mutationTable, mutationStatusTable,
791                     srcMetadataTable, fq_from, fq, status, kinds.map!(a => cast(int) a));
792         }();
793 
794         typeof(return) rval;
795         auto stmt = db.prepare(query);
796         if (file.length != 0)
797             stmt.get.bind(":path", file);
798         auto res = stmt.get.execute;
799         if (!res.empty)
800             rval = MetadataNoMutEntry(res.front.peek!long(0));
801         return rval;
802     }
803 
804     /// ditto.
805     alias aliveNoMutSrcMutants = countNoMutMutants!([Mutation.Status.alive], true);
806 
807     /// Returns: mutants killed by the test case.
808     MutationStatusId[] testCaseKilledSrcMutants(const Mutation.Kind[] kinds, TestCase tc) @trusted {
809         const query = format("
810             SELECT t1.id
811             FROM %s t0, %s t1, %s t2, %s t3
812             WHERE
813             t0.st_id = t1.id AND
814             t1.status = :st AND
815             t0.kind IN (%(%s,%)) AND
816             t2.name = :name AND
817             t2.id = t3.tc_id AND
818             t3.st_id = t1.id
819             GROUP BY t1.id", mutationTable, mutationStatusTable,
820                 allTestCaseTable, killedTestCaseTable, kinds.map!(a => cast(int) a));
821 
822         auto stmt = db.prepare(query);
823         stmt.get.bind(":st", cast(long) Mutation.Status.killed);
824         stmt.get.bind(":name", tc.name);
825 
826         auto app = appender!(MutationStatusId[])();
827         foreach (res; stmt.get.execute)
828             app.put(MutationStatusId(res.peek!long(0)));
829 
830         return app.data;
831     }
832 
833     /// Returns: mutants at mutations points that the test case has killed mutants at.
834     alias testCaseMutationPointAliveSrcMutants = testCaseCountSrcMutants!([
835             Mutation.Status.alive
836             ]);
837     /// ditto
838     alias testCaseMutationPointTimeoutSrcMutants = testCaseCountSrcMutants!(
839             [Mutation.Status.timeout]);
840     /// ditto
841     alias testCaseMutationPointKilledSrcMutants = testCaseCountSrcMutants!([
842             Mutation.Status.killed
843             ]);
844     /// ditto
845     alias testCaseMutationPointUnknownSrcMutants = testCaseCountSrcMutants!(
846             [Mutation.Status.unknown]);
847     /// ditto
848     alias testCaseMutationPointKilledByCompilerSrcMutants = testCaseCountSrcMutants!(
849             [Mutation.Status.killedByCompiler]);
850     /// ditto
851     alias testCaseMutationPointTotalSrcMutants = testCaseCountSrcMutants!(
852             [
853             Mutation.Status.alive, Mutation.Status.killed, Mutation.Status.timeout
854             ]);
855 
856     private MutationStatusId[] testCaseCountSrcMutants(int[] status)(
857             const Mutation.Kind[] kinds, TestCase tc) @trusted {
858         const query = format("
859             SELECT t1.id
860             FROM %s t0, %s t1
861             WHERE
862             t0.mp_id IN (SELECT t1.id
863                       FROM %s t0,%s t1, %s t2, %s t3
864                       WHERE
865                       t0.mp_id = t1.id AND
866                       t2.name = :name AND
867                       t2.id = t3.tc_id AND
868                       t3.st_id = t0.st_id
869                       )
870             AND
871             t0.st_id = t1.id AND
872             t1.status IN (%(%s,%)) AND
873             t0.kind IN (%(%s,%))
874             GROUP BY t1.id", mutationTable, mutationStatusTable, mutationTable, mutationPointTable,
875                 allTestCaseTable, killedTestCaseTable, status, kinds.map!(a => cast(int) a));
876 
877         auto stmt = db.prepare(query);
878         stmt.get.bind(":name", tc.name);
879 
880         auto app = appender!(MutationStatusId[])();
881         foreach (res; stmt.get.execute)
882             app.put(MutationStatusId(res.peek!long(0)));
883 
884         return app.data;
885     }
886 
887     Nullable!Checksum getChecksum(MutationStatusId id) @trusted {
888         immutable sql = format!"SELECT checksum0, checksum1 FROM %s WHERE id=:id"(
889                 mutationStatusTable);
890         auto stmt = db.prepare(sql);
891         stmt.get.bind(":id", cast(long) id);
892 
893         typeof(return) rval;
894         foreach (res; stmt.get.execute) {
895             rval = Checksum(res.peek!long(0), res.peek!long(1));
896             break;
897         }
898         return rval;
899     }
900 
901     Nullable!Checksum getFileChecksum(const Path p) @trusted {
902         immutable sql = format!"SELECT checksum0,checksum1 FROM %s WHERE path=:path"(filesTable);
903         auto stmt = db.prepare(sql);
904         stmt.get.bind(":path", cast(string) p);
905         auto res = stmt.get.execute;
906 
907         typeof(return) rval;
908         if (!res.empty) {
909             rval = Checksum(res.front.peek!long(0), res.front.peek!long(1));
910         }
911 
912         return rval;
913     }
914 
915     void put(const Path p, Checksum cs, const Language lang) @trusted {
916         immutable sql = format!"INSERT OR IGNORE INTO %s (path, checksum0, checksum1, lang)
917             VALUES (:path, :checksum0, :checksum1, :lang)"(
918                 filesTable);
919         auto stmt = db.prepare(sql);
920         stmt.get.bind(":path", cast(string) p);
921         stmt.get.bind(":checksum0", cast(long) cs.c0);
922         stmt.get.bind(":checksum1", cast(long) cs.c1);
923         stmt.get.bind(":lang", cast(long) lang);
924         stmt.get.execute;
925     }
926 
927     /// Remove all metadata.
928     void clearMetadata() {
929         immutable sql = format!"DELETE FROM %s"(rawSrcMetadataTable);
930         db.run(sql);
931     }
932 
933     /** Save line metadata to the database which is used to associate line
934      * metadata with mutants.
935      */
936     void put(const LineMetadata[] mdata) {
937         import sumtype;
938 
939         // TODO: convert to microrm
940         enum sql = format!"INSERT OR IGNORE INTO %s
941             (file_id, line, nomut, tag, comment)
942             VALUES(:fid, :line, :nomut, :tag, :comment)"(
943                     rawSrcMetadataTable);
944 
945         auto stmt = db.prepare(sql);
946         foreach (meta; mdata) {
947             auto nomut = meta.attr.match!((NoMetadata a) => NoMut.init, (NoMut a) => a);
948             stmt.get.bindAll(cast(long) meta.id, meta.line, meta.isNoMut,
949                     nomut.tag, nomut.comment);
950             stmt.get.execute;
951             stmt.get.reset;
952         }
953     }
954 
955     /// Store all found mutants.
956     void put(MutationPointEntry2[] mps, AbsolutePath rel_dir) @trusted {
957         enum insert_mp_sql = format("INSERT OR IGNORE INTO %s
958             (file_id, offset_begin, offset_end, line, column, line_end, column_end)
959             SELECT id,:begin,:end,:line,:column,:line_end,:column_end
960             FROM %s
961             WHERE
962             path = :path", mutationPointTable, filesTable);
963         auto mp_stmt = db.prepare(insert_mp_sql);
964 
965         foreach (mp; mps) {
966             auto rel_file = relativePath(mp.file, rel_dir).Path;
967             mp_stmt.get.bind(":begin", mp.offset.begin);
968             mp_stmt.get.bind(":end", mp.offset.end);
969             mp_stmt.get.bind(":line", mp.sloc.line);
970             mp_stmt.get.bind(":column", mp.sloc.column);
971             mp_stmt.get.bind(":line_end", mp.slocEnd.line);
972             mp_stmt.get.bind(":column_end", mp.slocEnd.column);
973             mp_stmt.get.bind(":path", cast(string) rel_file);
974             mp_stmt.get.execute;
975             mp_stmt.get.reset;
976         }
977 
978         enum insert_cmut_sql = format("INSERT OR IGNORE INTO %s
979             (status,test_cnt,update_ts,added_ts,checksum0,checksum1)
980             VALUES(:st,0,:update_ts,:added_ts,:c0,:c1)",
981                     mutationStatusTable);
982         auto cmut_stmt = db.prepare(insert_cmut_sql);
983         const ts = Clock.currTime.toUTC.toSqliteDateTime;
984         cmut_stmt.get.bind(":st", Mutation.Status.unknown);
985         cmut_stmt.get.bind(":update_ts", ts);
986         cmut_stmt.get.bind(":added_ts", ts);
987         foreach (cm; mps.map!(a => a.cms).joiner) {
988             cmut_stmt.get.bind(":c0", cast(long) cm.id.c0);
989             cmut_stmt.get.bind(":c1", cast(long) cm.id.c1);
990             cmut_stmt.get.execute;
991             cmut_stmt.get.reset;
992         }
993 
994         enum insert_m_sql = format("INSERT OR IGNORE INTO %s
995             (mp_id, st_id, kind)
996             SELECT t0.id,t1.id,:kind FROM %s t0, %s t1, %s t2 WHERE
997             t2.path = :path AND
998             t0.file_id = t2.id AND
999             t0.offset_begin = :off_begin AND
1000             t0.offset_end = :off_end AND
1001             t1.checksum0 = :c0 AND
1002             t1.checksum1 = :c1", mutationTable,
1003                     mutationPointTable, mutationStatusTable, filesTable);
1004         auto insert_m = db.prepare(insert_m_sql);
1005 
1006         foreach (mp; mps) {
1007             foreach (m; mp.cms) {
1008                 auto rel_file = relativePath(mp.file, rel_dir).Path;
1009                 insert_m.get.bind(":path", cast(string) rel_file);
1010                 insert_m.get.bind(":off_begin", mp.offset.begin);
1011                 insert_m.get.bind(":off_end", mp.offset.end);
1012                 insert_m.get.bind(":c0", cast(long) m.id.c0);
1013                 insert_m.get.bind(":c1", cast(long) m.id.c1);
1014                 insert_m.get.bind(":kind", m.mut.kind);
1015                 insert_m.get.execute;
1016                 insert_m.get.reset;
1017             }
1018         }
1019     }
1020 
1021     /** Remove all mutants points from the database.
1022      *
1023      * This removes all the mutants because of the cascade delete of the
1024      * tables. But it will keep the mutation statuses and thus the checksums
1025      * and the status of the code changes.
1026      *
1027      * This then mean that when mutations+mutation points are added back they
1028      * may reconnect with a mutation status.
1029      */
1030     void removeAllMutationPoints() @trusted {
1031         enum sql = format!"DELETE FROM %s"(mutationPointTable);
1032         db.run(sql);
1033     }
1034 
1035     /// ditto
1036     void removeAllFiles() @trusted {
1037         enum sql = format!"DELETE FROM %s"(filesTable);
1038         db.run(sql);
1039     }
1040 
1041     /// Remove mutants that have no connection to a mutation point, orphened mutants.
1042     void removeOrphanedMutants() @trusted {
1043         enum sql = format!"DELETE FROM %s WHERE id NOT IN (SELECT st_id FROM %s)"(
1044                     mutationStatusTable, mutationTable);
1045         db.run(sql);
1046     }
1047 
1048     /** Add a link between the mutation and what test case killed it.
1049      *
1050      * Params:
1051      *  id = ?
1052      *  tcs = test cases to add
1053      */
1054     void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted {
1055         if (tcs.length == 0)
1056             return;
1057 
1058         immutable st_id = () {
1059             enum st_id_for_mutation_q = format("SELECT st_id FROM %s WHERE id=:id", mutationTable);
1060             auto stmt = db.prepare(st_id_for_mutation_q);
1061             stmt.get.bind(":id", cast(long) id);
1062             return stmt.get.execute.oneValue!long;
1063         }();
1064         updateMutationTestCases(MutationStatusId(st_id), tcs);
1065     }
1066 
1067     /** Add a link between the mutation and what test case killed it.
1068      *
1069      * Params:
1070      *  id = ?
1071      *  tcs = test cases to add
1072      */
1073     void updateMutationTestCases(const MutationStatusId st_id, const(TestCase)[] tcs) @trusted {
1074         if (tcs.length == 0)
1075             return;
1076 
1077         try {
1078             enum remove_old_sql = format("DELETE FROM %s WHERE st_id=:id", killedTestCaseTable);
1079             auto stmt = db.prepare(remove_old_sql);
1080             stmt.get.bind(":id", cast(long) st_id);
1081             stmt.get.execute;
1082         } catch (Exception e) {
1083         }
1084 
1085         enum add_if_non_exist_tc_sql = format(
1086                     "INSERT INTO %s (name) SELECT :name1 WHERE NOT EXISTS (SELECT * FROM %s WHERE name = :name2)",
1087                     allTestCaseTable, allTestCaseTable);
1088         auto stmt_insert_tc = db.prepare(add_if_non_exist_tc_sql);
1089 
1090         enum add_new_sql = format(
1091                     "INSERT INTO %s (st_id, tc_id, location) SELECT :st_id,t1.id,:loc FROM %s t1 WHERE t1.name = :tc",
1092                     killedTestCaseTable, allTestCaseTable);
1093         auto stmt_insert = db.prepare(add_new_sql);
1094         foreach (const tc; tcs) {
1095             try {
1096                 stmt_insert_tc.get.reset;
1097                 stmt_insert_tc.get.bind(":name1", tc.name);
1098                 stmt_insert_tc.get.bind(":name2", tc.name);
1099                 stmt_insert_tc.get.execute;
1100 
1101                 stmt_insert.get.reset;
1102                 stmt_insert.get.bind(":st_id", cast(long) st_id);
1103                 stmt_insert.get.bind(":loc", tc.location);
1104                 stmt_insert.get.bind(":tc", tc.name);
1105                 stmt_insert.get.execute;
1106             } catch (Exception e) {
1107                 logger.warning(e.msg);
1108             }
1109         }
1110     }
1111 
1112     /** Set detected test cases.
1113      *
1114      * This will replace those that where previously stored.
1115      *
1116      * Returns: ID of affected mutation statuses.
1117      */
1118     MutationStatusId[] setDetectedTestCases(const(TestCase)[] tcs) @trusted {
1119         if (tcs.length == 0)
1120             return null;
1121 
1122         auto mut_status_ids = appender!(MutationStatusId[])();
1123 
1124         immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string;
1125         internalAddDetectedTestCases(tcs, tmp_name);
1126 
1127         immutable mut_st_id = format!"SELECT DISTINCT t1.st_id
1128             FROM %s t0, %s t1
1129             WHERE
1130             t0.name NOT IN (SELECT name FROM %s) AND
1131             t0.id = t1.tc_id"(allTestCaseTable,
1132                 killedTestCaseTable, tmp_name);
1133         auto stmt = db.prepare(mut_st_id);
1134         foreach (res; stmt.get.execute)
1135             mut_status_ids.put(res.peek!long(0).MutationStatusId);
1136 
1137         immutable remove_old_sql = format!"DELETE FROM %s WHERE name NOT IN (SELECT name FROM %s)"(
1138                 allTestCaseTable, tmp_name);
1139         db.run(remove_old_sql);
1140 
1141         db.run(format!"DROP TABLE %s"(tmp_name));
1142 
1143         return mut_status_ids.data;
1144     }
1145 
1146     /** Add test cases to those that have been detected.
1147      *
1148      * They will be added if they are unique.
1149      */
1150     void addDetectedTestCases(const(TestCase)[] tcs) @trusted {
1151         if (tcs.length == 0)
1152             return;
1153 
1154         immutable tmp_name = "tmp_new_tc_" ~ __LINE__.to!string;
1155         internalAddDetectedTestCases(tcs, tmp_name);
1156         db.run(format!"DROP TABLE %s"(tmp_name));
1157     }
1158 
1159     /// ditto.
1160     private void internalAddDetectedTestCases(const(TestCase)[] tcs, string tmp_tbl) @trusted {
1161         db.run(format!"CREATE TEMP TABLE %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"(
1162                 tmp_tbl));
1163 
1164         immutable add_tc_sql = format!"INSERT INTO %s (name) VALUES(:name)"(tmp_tbl);
1165         auto insert_s = db.prepare(add_tc_sql);
1166         foreach (tc; tcs) {
1167             insert_s.get.bind(":name", tc.name);
1168             insert_s.get.execute;
1169             insert_s.get.reset;
1170         }
1171 
1172         // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table
1173         //Q: What is happening here?
1174         //
1175         //A: Conceptually, we select all rows from table1 and for each row we
1176         //attempt to find a row in table2 with the same value for the name
1177         //column.  If there is no such row, we just leave the table2 portion of
1178         //our result empty for that row. Then we constrain our selection by
1179         //picking only those rows in the result where the matching row does not
1180         //exist. Finally, We ignore all fields from our result except for the
1181         //name column (the one we are sure that exists, from table1).
1182         //
1183         //While it may not be the most performant method possible in all cases,
1184         //it should work in basically every database engine ever that attempts
1185         //to implement ANSI 92 SQL
1186         immutable 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"(
1187                 allTestCaseTable, tmp_tbl, allTestCaseTable);
1188         db.run(add_missing_sql);
1189     }
1190 
1191     /// Returns: detected test cases.
1192     TestCase[] getDetectedTestCases() @trusted {
1193         auto rval = appender!(TestCase[])();
1194         db.run(select!AllTestCaseTbl).map!(a => TestCase(a.name)).copy(rval);
1195         return rval.data;
1196     }
1197 
1198     /// Returns: detected test cases.
1199     TestCaseId[] getDetectedTestCaseIds() @trusted {
1200         auto rval = appender!(TestCaseId[])();
1201         db.run(select!AllTestCaseTbl).map!(a => TestCaseId(a.id)).copy(rval);
1202         return rval.data;
1203     }
1204 
1205     /// Returns: test cases that has killed zero mutants.
1206     TestCase[] getTestCasesWithZeroKills() @trusted {
1207         enum sql = format("SELECT t1.name FROM %s t1 WHERE t1.id NOT IN (SELECT tc_id FROM %s)",
1208                     allTestCaseTable, killedTestCaseTable);
1209 
1210         auto rval = appender!(TestCase[])();
1211         auto stmt = db.prepare(sql);
1212         foreach (a; stmt.get.execute)
1213             rval.put(TestCase(a.peek!string(0)));
1214 
1215         return rval.data;
1216     }
1217 
1218     /** Guarantees that the there are no duplications of `TestCaseId`.
1219      *
1220      * Returns: test cases that has killed at least one mutant.
1221      */
1222     TestCaseId[] getTestCasesWithAtLeastOneKill(const Mutation.Kind[] kinds) @trusted {
1223         immutable sql = format!"SELECT DISTINCT t1.id
1224             FROM %s t1, %s t2, %s t3
1225             WHERE
1226             t1.id = t2.tc_id AND
1227             t2.st_id == t3.st_id AND
1228             t3.kind IN (%(%s,%))"(allTestCaseTable,
1229                 killedTestCaseTable, mutationTable, kinds.map!(a => cast(int) a));
1230 
1231         auto rval = appender!(TestCaseId[])();
1232         auto stmt = db.prepare(sql);
1233         foreach (a; stmt.get.execute)
1234             rval.put(TestCaseId(a.peek!long(0)));
1235 
1236         return rval.data;
1237     }
1238 
1239     /// Returns: the name of the test case.
1240     string getTestCaseName(const TestCaseId id) @trusted {
1241         enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable);
1242         auto stmt = db.prepare(sql);
1243         stmt.get.bind(":id", cast(long) id);
1244         auto res = stmt.get.execute;
1245         return res.oneValue!string;
1246     }
1247 
1248     /// Returns: stats about the test case.
1249     Nullable!TestCaseInfo getTestCaseInfo(const TestCase tc, const Mutation.Kind[] kinds) @trusted {
1250         const sql = format("SELECT sum(t2.time),count(t1.st_id)
1251             FROM %s t0, %s t1, %s t2, %s t3
1252             WHERE
1253             t0.name = :name AND
1254             t0.id = t1.tc_id AND
1255             t1.st_id = t2.id AND
1256             t2.id = t3.st_id AND
1257             t3.kind IN (%(%s,%))", allTestCaseTable,
1258                 killedTestCaseTable, mutationStatusTable, mutationTable,
1259                 kinds.map!(a => cast(int) a));
1260         auto stmt = db.prepare(sql);
1261         stmt.get.bind(":name", tc.name);
1262 
1263         typeof(return) rval;
1264         foreach (a; stmt.get.execute)
1265             rval = TestCaseInfo(a.peek!long(0).dur!"msecs", a.peek!long(1));
1266         return rval;
1267     }
1268 
1269     /// Returns: all test cases for the file and the mutants they killed.
1270     TestCaseInfo2[] getAllTestCaseInfo2(const FileId file, const Mutation.Kind[] kinds) @trusted {
1271         // row of test case name and mutation id.
1272         const sql = format("SELECT t0.name,t3.id
1273             FROM %s t0, %s t1, %s t2, %s t3, %s t4
1274             WHERE
1275             t0.id = t1.tc_id AND
1276             t1.st_id = t2.id AND
1277             t2.id = t3.st_id AND
1278             t4.id = :file_id AND
1279             t3.kind IN (%(%s,%))", allTestCaseTable, killedTestCaseTable,
1280                 mutationStatusTable, mutationTable, filesTable, kinds.map!(a => cast(int) a));
1281         auto stmt = db.prepare(sql);
1282         stmt.get.bind(":file_id", cast(long) file);
1283 
1284         MutationId[][string] data;
1285         foreach (row; stmt.get.execute) {
1286             const name = row.peek!string(0);
1287             if (auto v = name in data) {
1288                 *v ~= MutationId(row.peek!long(1));
1289             } else {
1290                 data[name] = [MutationId(row.peek!long(1))];
1291             }
1292         }
1293 
1294         auto app = appender!(TestCaseInfo2[])();
1295         data.byKeyValue.map!(a => TestCaseInfo2(TestCase(a.key), a.value)).copy(app);
1296         return app.data;
1297     }
1298 
1299     /// Returns: the test case.
1300     Nullable!TestCase getTestCase(const TestCaseId id) @trusted {
1301         enum sql = format!"SELECT name FROM %s WHERE id = :id"(allTestCaseTable);
1302         auto stmt = db.prepare(sql);
1303         stmt.get.bind(":id", cast(long) id);
1304 
1305         typeof(return) rval;
1306         foreach (res; stmt.get.execute) {
1307             rval = TestCase(res.peek!string(0));
1308         }
1309         return rval;
1310     }
1311 
1312     /// Returns: the test case id.
1313     Nullable!TestCaseId getTestCaseId(const TestCase tc) @trusted {
1314         enum sql = format!"SELECT id FROM %s WHERE name = :name"(allTestCaseTable);
1315         auto stmt = db.prepare(sql);
1316         stmt.get.bind(":name", tc.name);
1317 
1318         typeof(return) rval;
1319         foreach (res; stmt.get.execute) {
1320             rval = TestCaseId(res.peek!long(0));
1321         }
1322         return rval;
1323     }
1324 
1325     /// The mutation ids are guaranteed to be sorted.
1326     /// Returns: the mutants the test case killed.
1327     MutationId[] getTestCaseMutantKills(const TestCaseId id, const Mutation.Kind[] kinds) @trusted {
1328         immutable sql = format!"SELECT t2.id
1329             FROM %s t1, %s t2
1330             WHERE
1331             t1.tc_id = :tid AND
1332             t1.st_id = t2.st_id AND
1333             t2.kind IN (%(%s,%))
1334             ORDER BY
1335             t2.id"(killedTestCaseTable,
1336                 mutationTable, kinds.map!(a => cast(int) a));
1337 
1338         auto rval = appender!(MutationId[])();
1339         auto stmt = db.prepare(sql);
1340         stmt.get.bind(":tid", cast(long) id);
1341         foreach (a; stmt.get.execute)
1342             rval.put(MutationId(a.peek!long(0)));
1343 
1344         return rval.data;
1345     }
1346 
1347     /// Returns: test cases that killed the mutant.
1348     TestCase[] getTestCases(const MutationId id) @trusted {
1349         Appender!(TestCase[]) rval;
1350 
1351         enum get_test_cases_sql = format!"SELECT t1.name,t2.location
1352             FROM %s t1, %s t2, %s t3
1353             WHERE
1354             t3.id = :id AND
1355             t3.st_id = t2.st_id AND
1356             t2.tc_id = t1.id"(allTestCaseTable,
1357                     killedTestCaseTable, mutationTable);
1358         auto stmt = db.prepare(get_test_cases_sql);
1359         stmt.get.bind(":id", cast(long) id);
1360         foreach (a; stmt.get.execute)
1361             rval.put(TestCase(a.peek!string(0), a.peek!string(1)));
1362 
1363         return rval.data;
1364     }
1365 
1366     /** Returns: number of test cases
1367      */
1368     long getNumOfTestCases() @trusted {
1369         enum num_test_cases_sql = format!"SELECT count(*) FROM %s"(allTestCaseTable);
1370         return db.execute(num_test_cases_sql).oneValue!long;
1371     }
1372 
1373     /** Returns: test cases that killed other mutants at the same mutation point as `id`.
1374       */
1375     TestCase[] getSurroundingTestCases(const MutationId id) @trusted {
1376         Appender!(TestCase[]) rval;
1377 
1378         // TODO: optimize this. should be able to merge the two first queries to one.
1379 
1380         // get the mutation point ID that id reside at
1381         long mp_id;
1382         {
1383             auto stmt = db.prepare(format!"SELECT mp_id FROM %s WHERE id=:id"(mutationTable));
1384             stmt.get.bind(":id", cast(long) id);
1385             auto res = stmt.get.execute;
1386             if (res.empty)
1387                 return null;
1388             mp_id = res.oneValue!long;
1389         }
1390 
1391         // get all the mutation status ids at the mutation point
1392         long[] mut_st_ids;
1393         {
1394             auto stmt = db.prepare(format!"SELECT st_id FROM %s WHERE mp_id=:id"(mutationTable));
1395             stmt.get.bind(":id", mp_id);
1396             auto res = stmt.get.execute;
1397             if (res.empty)
1398                 return null;
1399             mut_st_ids = res.map!(a => a.peek!long(0)).array;
1400         }
1401 
1402         // get all the test cases that are killed at the mutation point
1403         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 (%(%s,%))"(
1404                 killedTestCaseTable, allTestCaseTable, mut_st_ids);
1405         auto stmt = db.prepare(get_test_cases_sql);
1406         foreach (a; stmt.get.execute) {
1407             rval.put(TestCase(a.peek!string(0), a.peek!string(1)));
1408         }
1409 
1410         return rval.data;
1411     }
1412 
1413     void removeTestCase(const Regex!char rex, const(Mutation.Kind)[] kinds) @trusted {
1414         immutable sql = format!"SELECT t1.id,t1.name FROM %s t1,%s t2, %s t3 WHERE t1.id = t2.tc_id AND t2.st_id = t3.st_id AND t3.kind IN (%(%s,%))"(
1415                 allTestCaseTable, killedTestCaseTable, mutationTable,
1416                 kinds.map!(a => cast(long) a));
1417         auto stmt = db.prepare(sql);
1418 
1419         auto del_stmt = db.prepare(format!"DELETE FROM %s WHERE id=:id"(allTestCaseTable));
1420         foreach (row; stmt.get.execute) {
1421             string tc = row.peek!string(1);
1422             if (tc.matchFirst(rex).empty)
1423                 continue;
1424 
1425             long id = row.peek!long(0);
1426             del_stmt.get.bind(":id", id);
1427             del_stmt.get.execute;
1428             del_stmt.get.reset;
1429         }
1430     }
1431 
1432     /// Returns: the context for the timeout algorithm.
1433     MutantTimeoutCtx getMutantTimeoutCtx() @trusted {
1434         foreach (res; db.run(select!MutantTimeoutCtx))
1435             return res;
1436         return MutantTimeoutCtx.init;
1437     }
1438 
1439     void putMutantTimeoutCtx(const MutantTimeoutCtx ctx) @trusted {
1440         db.run(delete_!MutantTimeoutCtx);
1441         db.run(insert!MutantTimeoutCtx.insert, ctx);
1442     }
1443 
1444     void putMutantInTimeoutWorklist(const MutationStatusId id) @trusted {
1445         const sql = format!"INSERT OR IGNORE INTO %s (id) VALUES (:id)"(mutantTimeoutWorklistTable);
1446         auto stmt = db.prepare(sql);
1447         stmt.get.bind(":id", cast(long) id);
1448         stmt.get.execute;
1449     }
1450 
1451     /** Remove all mutants that are in the worklist that do NOT have the
1452      * mutation status timeout.
1453      */
1454     void reduceMutantTimeoutWorklist() @trusted {
1455         immutable sql = format!"DELETE FROM %1$s
1456             WHERE
1457             id IN (SELECT id FROM %2$s WHERE status != :status)"(
1458                 mutantTimeoutWorklistTable, mutationStatusTable);
1459         auto stmt = db.prepare(sql);
1460         stmt.get.bind(":status", cast(ubyte) Mutation.Status.timeout);
1461         stmt.get.execute;
1462     }
1463 
1464     /// Remove all mutants from the worklist.
1465     void clearMutantTimeoutWorklist() @trusted {
1466         immutable sql = format!"DELETE FROM %1$s"(mutantTimeoutWorklistTable);
1467         db.run(sql);
1468     }
1469 
1470     /// Returns: the number of mutants in the worklist.
1471     long countMutantTimeoutWorklist() @trusted {
1472         immutable sql = format!"SELECT count(*) FROM %1$s"(mutantTimeoutWorklistTable);
1473         auto stmt = db.prepare(sql);
1474         auto res = stmt.get.execute();
1475         return res.oneValue!long;
1476     }
1477 
1478     /// Changes the status of mutants in the timeout worklist to unknown.
1479     void resetMutantTimeoutWorklist() @trusted {
1480         immutable sql = format!"UPDATE %1$s SET status=:st WHERE id IN (SELECT id FROM %2$s)"(
1481                 mutationStatusTable, mutantTimeoutWorklistTable);
1482         auto stmt = db.prepare(sql);
1483         stmt.get.bind(":st", cast(ubyte) Mutation.Status.unknown);
1484         stmt.get.execute;
1485     }
1486 
1487     /** Update the content of metadata tables with what has been added to the
1488      * raw table data.
1489      */
1490     void updateMetadata() @trusted {
1491         db.run(format!"DELETE FROM %s"(srcMetadataTable));
1492         db.run(format!"DELETE FROM %s"(nomutTable));
1493         db.run(format!"DELETE FROM %s"(nomutDataTable));
1494 
1495         immutable nomut_tbl = "INSERT INTO %s
1496             SELECT
1497                 t0.id mp_id,
1498                 t1.line line,
1499                 count(*) status
1500                 FROM %s t0, %s t1
1501                 WHERE
1502                 t0.file_id = t1.file_id AND
1503                 (t1.line BETWEEN t0.line AND t0.line_end)
1504                 GROUP BY
1505                 t0.id";
1506         db.run(format!nomut_tbl(nomutTable, mutationPointTable, rawSrcMetadataTable));
1507 
1508         immutable src_metadata_sql = "INSERT INTO %s
1509             SELECT DISTINCT
1510             t0.id AS mut_id,
1511             t1.id AS st_id,
1512             t2.id AS mp_id,
1513             t3.id AS file_id,
1514             (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut
1515             FROM %s t0, %s t1, %s t2, %s t3
1516             WHERE
1517             t0.mp_id = t2.id AND
1518             t0.st_id = t1.id AND
1519             t2.file_id = t3.id";
1520         db.run(format!src_metadata_sql(srcMetadataTable, nomutTable,
1521                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
1522 
1523         immutable nomut_data_tbl = "INSERT INTO %s
1524             SELECT
1525                 t0.id as mut_id,
1526                 t0.mp_id as mp_id,
1527                 t1.line as line,
1528                 t1.tag as tag,
1529                 t1.comment as comment
1530                 FROM %s t0, %s t1, %s t2
1531                 WHERE
1532                 t0.mp_id = t2.mp_id AND
1533                 t1.line = t2.line";
1534         db.run(format!nomut_data_tbl(nomutDataTable, mutationTable,
1535                 rawSrcMetadataTable, nomutTable));
1536     }
1537 }
1538 
1539 private:
1540 
1541 MarkedMutant make(MarkedMutantTbl m) {
1542     import dextool.plugin.mutate.backend.type;
1543 
1544     return MarkedMutant(m.mutationStatusId.MutationStatusId, Checksum(m.checksum0,
1545             m.checksum1), m.mutationId.MutationId, SourceLoc(m.line, m.column),
1546             m.path.Path, m.toStatus.to!(Mutation.Status), m.time,
1547             m.rationale.Rationale, m.mutText);
1548 }