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