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