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