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 The only acceptable dependency are:
14  * ../type.d
15  * ..backend/type.d
16  * ../database/type.d
17  * ../database/schema.d
18 */
19 module dextool.plugin.mutate.backend.database.standalone;
20 
21 import core.time : Duration;
22 import logger = std.experimental.logger;
23 
24 import d2sqlite3 : sqlDatabase = Database;
25 
26 import dextool.type : AbsolutePath, Path;
27 
28 import dextool.plugin.mutate.backend.database.schema;
29 import dextool.plugin.mutate.backend.database.type;
30 
31 /** Database wrapper with minimal dependencies.
32  */
33 struct Database {
34     import std.conv : to;
35     import std.exception : collectException;
36     import std.typecons : Nullable;
37     import dextool.plugin.mutate.backend.type : MutationPoint, Mutation,
38         Checksum;
39 
40     sqlDatabase* db;
41     alias db this;
42 
43     /** Create a database by either opening an existing or initializing a new.
44      *
45      * Params:
46      *  db = path to the database
47      */
48     static auto make(string db) @safe {
49         return Database(initializeDB(db));
50     }
51 
52     // Not movable. The database should only be passed around as a reference,
53     // if at all.
54     @disable this(this);
55 
56     ~this() @trusted {
57         destroy(db);
58     }
59 
60     /// If the file has already been analyzed.
61     bool isAnalyzed(const Path p) @trusted {
62         auto stmt = db.prepare("SELECT count(*) FROM files WHERE path=:path LIMIT 1");
63         stmt.bind(":path", cast(string) p);
64         auto res = stmt.execute;
65         return res.oneValue!long != 0;
66     }
67 
68     /// If the file has already been analyzed.
69     bool isAnalyzed(const Path p, const Checksum cs) @trusted {
70         auto stmt = db.prepare(
71                 "SELECT count(*) FROM files WHERE path=:path AND checksum0=:cs0 AND checksum1=:cs1 LIMIT 1");
72         stmt.bind(":path", cast(string) p);
73         stmt.bind(":cs0", cs.c0);
74         stmt.bind(":cs1", cs.c1);
75         auto res = stmt.execute;
76         return res.oneValue!long != 0;
77     }
78 
79     Nullable!FileId getFileId(const Path p) @trusted {
80         auto stmt = db.prepare("SELECT id FROM files WHERE path=:path");
81         stmt.bind(":path", cast(string) p);
82         auto res = stmt.execute;
83 
84         typeof(return) rval;
85         if (!res.empty) {
86             rval = FileId(res.oneValue!long);
87         }
88 
89         return rval;
90     }
91 
92     /// Remove the file with all mutations that are coupled to it.
93     void removeFile(const Path p) @trusted {
94         auto stmt = db.prepare("DELETE FROM files WHERE path=:path");
95         stmt.bind(":path", cast(string) p);
96         stmt.execute;
97     }
98 
99     /// Returns: All files in the database as relative paths.
100     Path[] getFiles() @trusted {
101         import std.array : appender;
102 
103         auto stmt = db.prepare("SELECT path from files");
104         auto res = stmt.execute;
105 
106         auto app = appender!(Path[]);
107         foreach (ref r; res) {
108             app.put(Path(r.peek!string(0)));
109         }
110 
111         return app.data;
112     }
113 
114     /** Update the status of a mutant.
115      * Params:
116      *  id = ?
117      *  st = ?
118      *  d = time spent on veryfing the mutant
119      */
120     void updateMutation(const MutationId id, const Mutation.Status st,
121             const Duration d, const(TestCase)[] tcs) @trusted {
122         auto stmt = db.prepare(
123                 "UPDATE mutation SET status=:st,time=:time WHERE mutation.id == :id");
124         stmt.bind(":st", st.to!long);
125         stmt.bind(":id", id.to!long);
126         stmt.bind(":time", d.total!"msecs");
127         stmt.execute;
128 
129         updateMutationTestCases(id, tcs);
130     }
131 
132     /** Update the status of a mutant and broadcast the status to other mutants at that point.
133      *
134      * Params:
135      *  bcast = mutants to broadcast the status to in addition to the id
136      */
137     void updateMutationBroadcast(const MutationId id, const Mutation.Status st,
138             const Duration d, const(TestCase)[] tcs, const(Mutation.Kind)[] bcast) @trusted {
139         import std.algorithm : map;
140         import std.array : array;
141         import std.format : format;
142 
143         if (bcast.length == 1) {
144             updateMutation(id, st, d, tcs);
145             return;
146         }
147 
148         auto stmt = db.prepare("SELECT mp_id FROM mutation WHERE id=:id");
149         stmt.bind(":id", id.to!long);
150         auto res = stmt.execute;
151 
152         if (res.empty)
153             return;
154         long mp_id = res.front.peek!long(0);
155 
156         stmt = db.prepare(format("SELECT id FROM mutation WHERE mp_id=:id AND kind IN (%(%s,%))",
157                 bcast.map!(a => cast(int) a)));
158         stmt.bind(":id", mp_id);
159         res = stmt.execute;
160 
161         if (res.empty)
162             return;
163 
164         auto mut_ids = res.map!(a => a.peek!long(0).MutationId).array;
165 
166         stmt = db.prepare(format("UPDATE mutation SET status=:st,time=:time WHERE id IN (%(%s,%))",
167                 mut_ids.map!(a => cast(long) a)));
168         stmt.bind(":st", st.to!long);
169         stmt.bind(":time", d.total!"msecs");
170         stmt.execute;
171 
172         foreach (const mut_id; mut_ids) {
173             updateMutationTestCases(mut_id, tcs);
174         }
175     }
176 
177     Nullable!MutationEntry getMutation(const MutationId id) nothrow @trusted {
178         import dextool.plugin.mutate.backend.type;
179         import dextool.type : FileName;
180 
181         typeof(return) rval;
182 
183         try {
184             auto stmt = db.prepare("SELECT
185                                    mutation.id,
186                                    mutation.kind,
187                                    mutation.time,
188                                    mutation_point.offset_begin,
189                                    mutation_point.offset_end,
190                                    mutation_point.line,
191                                    mutation_point.column,
192                                    files.path
193                                    FROM mutation,mutation_point,files
194                                    WHERE
195                                    mutation.id == :id AND
196                                    mutation.mp_id == mutation_point.id AND
197                                    mutation_point.file_id == files.id");
198             stmt.bind(":id", cast(long) id);
199             auto res = stmt.execute;
200             if (res.empty)
201                 return rval;
202 
203             auto v = res.front;
204 
205             auto mp = MutationPoint(Offset(v.peek!uint(3), v.peek!uint(4)));
206             mp.mutations = [Mutation(v.peek!long(1).to!(Mutation.Kind))];
207             auto pkey = MutationId(v.peek!long(0));
208             auto file = Path(FileName(v.peek!string(7)));
209             auto sloc = SourceLoc(v.peek!uint(5), v.peek!uint(6));
210 
211             import core.time : dur;
212 
213             rval = MutationEntry(pkey, file, sloc, mp, v.peek!long(2).dur!"msecs");
214         }
215         catch (Exception e) {
216             logger.warning(e.msg).collectException;
217         }
218 
219         return rval;
220     }
221 
222     /** Remove all mutations of kinds.
223      */
224     void removeMutant(const Mutation.Kind[] kinds) @trusted {
225         import std.algorithm : map;
226         import std.format : format;
227 
228         auto s = format("DELETE FROM mutation_point WHERE id IN (SELECT mp_id FROM mutation WHERE kind IN (%(%s,%)))",
229                 kinds.map!(a => cast(int) a));
230         auto stmt = db.prepare(s);
231         stmt.execute;
232     }
233 
234     /** Reset all mutations of kinds with the status `st` to unknown.
235      */
236     void resetMutant(const Mutation.Kind[] kinds, Mutation.Status st, Mutation.Status to_st) @trusted {
237         import std.algorithm : map;
238         import std.format : format;
239 
240         auto s = format("UPDATE mutation SET status=%s WHERE status == %s AND kind IN (%(%s,%))",
241                 to_st.to!long, st.to!long, kinds.map!(a => cast(int) a));
242         auto stmt = db.prepare(s);
243         stmt.execute;
244     }
245 
246     import dextool.plugin.mutate.backend.type;
247 
248     alias aliveMutants = countMutants!(Mutation.Status.alive);
249     alias killedMutants = countMutants!(Mutation.Status.killed);
250     alias timeoutMutants = countMutants!(Mutation.Status.timeout);
251     alias unknownMutants = countMutants!(Mutation.Status.unknown);
252     alias killedByCompilerMutants = countMutants!(Mutation.Status.killedByCompiler);
253 
254     private Nullable!MutationReportEntry countMutants(int status)(const Mutation.Kind[] kinds) nothrow @trusted {
255         import core.time : dur;
256         import std.algorithm : map;
257         import std.format : format;
258 
259         enum query = format("SELECT count(*),sum(mutation.time) FROM mutation WHERE status==%s AND kind IN (%s)",
260                     status, "%(%s,%)");
261 
262         typeof(return) rval;
263         try {
264             auto stmt = db.prepare(format(query, kinds.map!(a => cast(int) a)));
265             auto res = stmt.execute;
266             if (res.empty)
267                 return rval;
268             rval = MutationReportEntry(res.front.peek!long(0),
269                     res.front.peek!long(1).dur!"msecs");
270         }
271         catch (Exception e) {
272             logger.warning(e.msg).collectException;
273         }
274 
275         return rval;
276     }
277 
278     void put(const Path p, Checksum cs) @trusted {
279         if (isAnalyzed(p))
280             return;
281 
282         auto stmt = db.prepare(
283                 "INSERT INTO files (path, checksum0, checksum1) VALUES (:path, :checksum0, :checksum1)");
284         stmt.bind(":path", cast(string) p);
285         stmt.bind(":checksum0", cast(long) cs.c0);
286         stmt.bind(":checksum1", cast(long) cs.c1);
287         stmt.execute;
288     }
289 
290     /** Save mutation points found in a specific file.
291      *
292      * Note: this assumes a file is never added more than once.
293      * If it where ever to be the mutation points would be duplicated.
294      *
295      * trusted: the d2sqlite3 interface is assumed to work correctly when the
296      * data via bind is *ok*.
297      */
298     void put(const(MutationPointEntry)[] mps, AbsolutePath rel_dir) @trusted {
299         import std.path : relativePath;
300 
301         auto mp_stmt = db.prepare("INSERT INTO mutation_point (file_id, offset_begin, offset_end, line, column) VALUES (:fid, :begin, :end, :line, :column)");
302         auto m_stmt = db.prepare(
303                 "INSERT INTO mutation (mp_id, kind, status) VALUES (:mp_id, :kind, :status)");
304 
305         db.begin;
306         scope (success)
307             db.commit;
308         scope (failure)
309             db.rollback;
310 
311         FileId[Path] file_ids;
312         foreach (a; mps) {
313             // remove mutation points that would never result in a mutation
314             if (a.mp.mutations.length == 0)
315                 continue;
316 
317             if (a.file is null) {
318                 debug logger.trace("this should not happen. The file is null file");
319                 continue;
320             }
321             auto rel_file = relativePath(a.file, rel_dir).Path;
322 
323             FileId id;
324             // assuming it is slow to lookup in the database so cache the lookups.
325             if (auto e = rel_file in file_ids) {
326                 id = *e;
327             } else {
328                 auto e = getFileId(rel_file);
329                 if (e.isNull) {
330                     // this only happens when the database is out of sync with
331                     // the filesystem or absolute paths are used.
332                     logger.errorf("File '%s' do not exist in the database",
333                             rel_file).collectException;
334                     continue;
335                 }
336                 id = e;
337                 file_ids[rel_file] = id;
338             }
339 
340             // fails if the constraint for mutation_point is violated
341             // TODO still a bit slow because this generates many exceptions.
342             try {
343                 const long mp_id = () {
344                     scope (exit)
345                         mp_stmt.reset;
346                     mp_stmt.bind(":fid", cast(long) id);
347                     mp_stmt.bind(":begin", a.mp.offset.begin);
348                     mp_stmt.bind(":end", a.mp.offset.end);
349                     mp_stmt.bind(":line", a.sloc.line);
350                     mp_stmt.bind(":column", a.sloc.column);
351                     mp_stmt.execute;
352                     return db.lastInsertRowid;
353                 }();
354 
355                 m_stmt.bind(":mp_id", mp_id);
356                 foreach (k; a.mp.mutations) {
357                     m_stmt.bind(":kind", k.kind);
358                     m_stmt.bind(":status", k.status);
359                     m_stmt.execute;
360                     m_stmt.reset;
361                 }
362             }
363             catch (Exception e) {
364             }
365         }
366     }
367 
368     /** Add a link between the mutation and what test case killed it.
369         Params:
370             id = ?
371             tcs = test cases to add
372       */
373     void updateMutationTestCases(const MutationId id, const(TestCase)[] tcs) @trusted {
374         import std.format : format;
375 
376         if (tcs.length == 0)
377             return;
378 
379         immutable mut_id = id.to!string;
380 
381         try {
382             immutable remove_old_sql = format("DELETE FROM %s WHERE mut_id=:id", testCaseTable);
383             auto stmt = db.prepare(remove_old_sql);
384             stmt.bind(":id", mut_id);
385             stmt.execute;
386         }
387         catch (Exception e) {
388         }
389 
390         immutable add_new_sql = format("INSERT INTO %s (mut_id, test_case) VALUES(:mut_id, :tc)",
391                 testCaseTable);
392         foreach (const tc; tcs) {
393             try {
394                 auto stmt = db.prepare(add_new_sql);
395                 stmt.bind(":mut_id", mut_id);
396                 stmt.bind(":tc", cast(string) tc);
397                 stmt.execute;
398             }
399             catch (Exception e) {
400                 logger.warning(e.msg);
401             }
402         }
403     }
404 
405     /** Returns: test cases that killed the mutant.
406       */
407     TestCase[] getTestCases(const MutationId id) @trusted {
408         import std.array : Appender;
409         import std.format : format;
410 
411         Appender!(TestCase[]) rval;
412 
413         immutable get_test_cases_sql = format("SELECT test_case FROM %s WHERE mut_id=:id",
414                 testCaseTable);
415         auto stmt = db.prepare(get_test_cases_sql);
416         stmt.bind(":id", cast(long) id);
417         foreach (a; stmt.execute) {
418             rval.put(TestCase(a.peek!string(0)));
419         }
420 
421         return rval.data;
422     }
423 
424     /** Returns: test cases that killed other mutants at the same mutation point as `id`.
425       */
426     TestCase[] getSurroundingTestCases(const MutationId id) @trusted {
427         import std.algorithm : map;
428         import std.array : Appender, array;
429         import std.format : format;
430 
431         Appender!(TestCase[]) rval;
432 
433         // TODO: optimize this. should be able to merge the two first queries to one.
434 
435         // get the mutation point ID that id reside at
436         long mp_id;
437         {
438             auto stmt = db.prepare(format("SELECT mp_id FROM %s WHERE id=:id", mutationTable));
439             stmt.bind(":id", cast(long) id);
440             auto res = stmt.execute;
441             if (res.empty)
442                 return null;
443             mp_id = res.oneValue!long;
444         }
445 
446         // get all the mutation ids at the mutation point
447         long[] mut_ids;
448         {
449             auto stmt = db.prepare(format("SELECT id FROM %s WHERE mp_id=:id", mutationTable));
450             stmt.bind(":id", mp_id);
451             auto res = stmt.execute;
452             if (res.empty)
453                 return null;
454             mut_ids = res.map!(a => a.peek!long(0)).array;
455         }
456 
457         // get all the test cases that are killed at the mutation point
458         immutable get_test_cases_sql = format("SELECT test_case FROM %s WHERE mut_id IN (%(%s,%))",
459                 testCaseTable, mut_ids);
460         auto stmt = db.prepare(get_test_cases_sql);
461         foreach (a; stmt.execute) {
462             rval.put(TestCase(a.peek!string(0)));
463         }
464 
465         return rval.data;
466     }
467 
468     import std.regex : Regex;
469 
470     void removeTestCase(const Regex!char rex, const(Mutation.Kind)[] kinds) @trusted {
471         import std.algorithm : map;
472         import std.format : format;
473         import std.regex : matchFirst;
474 
475         immutable sql = format(
476                 "SELECT test_case.id,test_case.test_case FROM %s,%s WHERE %s.mut_id=%s.id AND %s.kind IN (%(%s,%))",
477                 testCaseTable, mutationTable,
478                 testCaseTable, mutationTable, mutationTable, kinds.map!(a => cast(long) a));
479         auto stmt = db.prepare(sql);
480 
481         foreach (row; stmt.execute) {
482             string tc = row.peek!string(1);
483             if (tc.matchFirst(rex).empty)
484                 continue;
485 
486             long id = row.peek!long(0);
487             auto del_stmt = db.prepare(format("DELETE FROM %s WHERE id=:id", testCaseTable));
488             del_stmt.bind(":id", id);
489             del_stmt.execute;
490         }
491     }
492 }