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