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 schema to initialize the database.
11 
12 To ensure that the upgrade path for a database always work a database is
13 created at the "lowest supported" and upgraded to the latest.
14 
15 # How to add schema change
16 
17 1. add an upgrade function, upgradeVX.
18 
19 The function makeUpgradeTable will then automatically find it and use it. X
20  **must** be the version upgrading FROM.
21 
22 # Style
23 A database schema upgrade path shall have a comment stating what date it was added.
24 Each change to the database schema must have an equal upgrade added.
25 
26 # Sqlite3
27 From the sqlite3 manual $(LINK https://www.sqlite.org/datatype3.html):
28 Each value stored in an SQLite database (or manipulated by the database
29 engine) has one of the following storage classes:
30 
31 NULL. The value is a NULL value.
32 
33 INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes
34 depending on the magnitude of the value.
35 
36 REAL. The value is a floating point value, stored as an 8-byte IEEE floating
37 point number.
38 
39 TEXT. The value is a text string, stored using the database encoding (UTF-8,
40 UTF-16BE or UTF-16LE).
41 
42 BLOB. The value is a blob of data, stored exactly as it was input.
43 
44 A storage class is more general than a datatype. The INTEGER storage class, for
45 example, includes 6 different integer datatypes of different lengths.  This
46 makes a difference on disk. But as soon as INTEGER values are read off of disk
47 and into memory for processing, they are converted to the most general datatype
48 (8-byte signed integer). And so for the most part, "storage class" is
49 indistinguishable from "datatype" and the two terms can be used
50 interchangeably.
51 */
52 module dextool.plugin.mutate.backend.database.schema;
53 
54 import logger = std.experimental.logger;
55 import std.array : array, empty;
56 import std.datetime : SysTime;
57 import std.exception : collectException;
58 import std.format : format;
59 
60 import dextool.plugin.mutate.backend.type : Language;
61 
62 import d2sqlite3 : SqlDatabase = Database;
63 import miniorm : Miniorm, TableName, buildSchema, ColumnParam, TableForeignKey,
64     TableConstraint, TablePrimaryKey, KeyRef, KeyParam, ColumnName, delete_, insert, select;
65 
66 immutable allTestCaseTable = "all_test_case";
67 immutable filesTable = "files";
68 immutable killedTestCaseTable = "killed_test_case";
69 immutable markedMutantTable = "marked_mutant";
70 immutable mutantTimeoutCtxTable = "mutant_timeout_ctx";
71 immutable mutantTimeoutWorklistTable = "mutant_timeout_worklist";
72 immutable mutationPointTable = "mutation_point";
73 immutable mutationStatusTable = "mutation_status";
74 immutable mutationTable = "mutation";
75 immutable nomutDataTable = "nomut_data";
76 immutable nomutTable = "nomut";
77 immutable rawSrcMetadataTable = "raw_src_metadata";
78 immutable schemaVersionTable = "schema_version";
79 immutable srcMetadataTable = "src_metadata";
80 immutable schemataMutantTable = "schemata_mutant";
81 immutable schemataWorkListTable = "schemata_worklist";
82 immutable schemataTable = "schemata";
83 immutable schemataFragmentTable = "schemata_fragment";
84 
85 private immutable testCaseTableV1 = "test_case";
86 
87 /** Initialize or open an existing database.
88  *
89  * Params:
90  *  p = path where to initialize a new database or open an existing
91  *
92  * Returns: an open sqlite3 database object.
93  */
94 Miniorm initializeDB(const string p) @trusted
95 in {
96     assert(p.length != 0);
97 }
98 do {
99     import d2sqlite3 : SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE;
100 
101     static void setPragmas(ref SqlDatabase db) {
102         // dfmt off
103         auto pragmas = [
104             // required for foreign keys with cascade to work
105             "PRAGMA foreign_keys=ON;",
106             // use two worker threads. Should improve performance a bit without having an adverse effect.
107             // this should probably be user configurable.
108             "PRAGMA threads = 2;",
109         ];
110         // dfmt on
111 
112         foreach (p; pragmas) {
113             db.run(p);
114         }
115     }
116 
117     SqlDatabase sqliteDb;
118 
119     try {
120         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE);
121     } catch (Exception e) {
122         logger.trace(e.msg);
123         logger.trace("Initializing a new sqlite3 database");
124         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
125     }
126 
127     auto db = Miniorm(sqliteDb);
128 
129     // TODO: remove all key off in upgrade schemas.
130     db.run("PRAGMA foreign_keys=OFF;");
131     upgrade(db);
132     setPragmas(sqliteDb);
133 
134     return db;
135 }
136 
137 package:
138 
139 // metadata about mutants that occur on a line extracted from the source code.
140 // It is intended to further refined.
141 // nomut = if the line should ignore mutants.
142 // tag = a user defined tag for a NOMUT.
143 // comment = a user defined comment.
144 @TableName(rawSrcMetadataTable)
145 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
146 @TableConstraint("unique_line_in_file UNIQUE (file_id, line)")
147 struct RawSrcMetadata {
148     long id;
149 
150     @ColumnName("file_id")
151     long fileId;
152 
153     @ColumnParam("")
154     uint line;
155 
156     @ColumnParam("")
157     long nomut;
158 
159     @ColumnParam("")
160     string tag;
161 
162     @ColumnParam("")
163     string comment;
164 }
165 
166 @TableName(srcMetadataTable)
167 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
168 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
169 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
170 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
171 struct SrcMetadataTable {
172     @ColumnName("mut_id")
173     long mutationId;
174 
175     @ColumnName("st_id")
176     long mutationStatusId;
177 
178     @ColumnName("mp_id")
179     long mutationPointId;
180 
181     @ColumnName("file_id")
182     long fileId;
183 
184     @ColumnName("nomut")
185     long nomutCount;
186 }
187 
188 // Reconstruct the nomut table in Miniorm.
189 @TableName(nomutTable)
190 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
191 struct NomutTbl {
192     @ColumnName("mp_id")
193     long mutationPointId;
194 
195     long line;
196 
197     /// != 0 when a nomut is tagged on the line.
198     long status;
199 }
200 
201 @TableName(nomutDataTable)
202 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
203 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
204 struct NomutDataTbl {
205     @ColumnName("mut_id")
206     long mutationId;
207 
208     @ColumnName("mp_id")
209     long mutationPointId;
210 
211     long line;
212 
213     @ColumnParam("")
214     string tag;
215 
216     @ColumnParam("")
217     string comment;
218 }
219 
220 @TableName(schemaVersionTable)
221 struct VersionTbl {
222     @ColumnName("version")
223     long version_;
224 }
225 
226 /// checksum is 128bit. Using a integer to better represent and search for them
227 /// in queries.
228 @TableName(filesTable)
229 @TableConstraint("unique_ UNIQUE (path)")
230 struct FilesTbl {
231     long id;
232 
233     @ColumnParam("")
234     string path;
235 
236     long checksum0;
237     long checksum1;
238     Language lang;
239 }
240 
241 /// there shall never exist two mutations points for the same file+offset.
242 @TableName(mutationPointTable)
243 @TableConstraint("file_offset UNIQUE (file_id, offset_begin, offset_end)")
244 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
245 struct MutationPointTbl {
246     long id;
247     long file_id;
248     uint offset_begin;
249     uint offset_end;
250 
251     /// line start from zero
252     @ColumnParam("")
253     uint line;
254     @ColumnParam("")
255     uint column;
256 
257     @ColumnParam("")
258     uint line_end;
259 
260     @ColumnParam("")
261     uint column_end;
262 }
263 
264 @TableName(mutationTable)
265 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
266 @TableForeignKey("st_id", KeyRef("mutation_status(id)"))
267 @TableConstraint("unique_ UNIQUE (mp_id, kind)")
268 struct MutationTbl {
269     long id;
270 
271     long mp_id;
272 
273     @ColumnParam("")
274     long st_id;
275 
276     long kind;
277 }
278 
279 /**
280  * This could use an intermediate adapter table to normalise the test_case data
281  * but I chose not to do that because it makes it harder to add test cases and
282  * do a cleanup.
283  */
284 @TableName(killedTestCaseTable)
285 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
286 @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE"))
287 struct TestCaseKilledTbl {
288     long id;
289 
290     @ColumnName("st_id")
291     long mutationStatusId;
292     @ColumnName("tc_id")
293     long testCaseId;
294 
295     // location is a filesystem location or other suitable helper for a user to
296     // locate the test.
297     @ColumnParam("")
298     string location;
299 }
300 
301 /**
302  * Track all test cases that has been found by the test suite output analyzer.
303  * Useful to find test cases that has never killed any mutant.
304  * name should match test_case_killed_v2_tbl
305  * TODO: name should be the primary key. on a conflict a counter should be updated.
306  */
307 @TableName(allTestCaseTable)
308 struct AllTestCaseTbl {
309     long id;
310 
311     @ColumnParam("")
312     string name;
313 }
314 
315 /**
316  * the status of a mutant. if it is killed or otherwise.
317  * multiple mutation operators can result in the same change of the source
318  * code. By coupling the mutant status to the checksum of the source code
319  * change it means that two mutations that have the same checksum will
320  * "cooperate".
321  * TODO: change the checksum to being NOT NULL in the future. Can't for now
322  * when migrating to schema version 5->6.
323  * time = ms spent on verifying the mutant
324  * timestamp = is when the status where last updated. Seconds at UTC+0.
325  * added_ts = when the mutant where added to the system. UTC+0.
326  * test_cnt = nr of times the mutant has been tested without being killed.
327  */
328 @TableName(mutationStatusTable)
329 @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
330 struct MutationStatusTbl {
331     long id;
332     long status;
333 
334     @ColumnParam("")
335     long time;
336 
337     @ColumnName("test_cnt")
338     long testCnt;
339 
340     @ColumnParam("")
341     @ColumnName("update_ts")
342     SysTime updated;
343 
344     @ColumnParam("")
345     @ColumnName("added_ts")
346     SysTime added;
347 
348     long checksum0;
349     long checksum1;
350 }
351 
352 @TableName(mutantTimeoutWorklistTable)
353 @TableForeignKey("id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
354 struct MutantTimeoutWorklistTbl {
355     long id;
356 }
357 
358 /** The defaults for the schema is the state that the state machine start in.
359  *
360  * This mean that if there are nothing in the database then `.init` is the correct starting point.
361  */
362 @TableName(mutantTimeoutCtxTable)
363 struct MutantTimeoutCtxTbl {
364     /// What iteration the timeout testing is at.
365     long iter;
366 
367     /// Last count of the mutants in the worklist that where in the timeout state.
368     long worklistCount;
369 
370     enum State {
371         init_,
372         running,
373         done
374     }
375 
376     /// State of the timeout algorithm.
377     State state;
378 }
379 
380 /** The lower 64bit of the checksum should be good enough as the primary key.
381  * By doing it this way it is easier to update a marked mutant without
382  * "peeking" in the database ("insert or update").
383  *
384  * Both `st_id` and `mut_id` are values that sqlite can reuse between analyzes
385  * if they have been previously removed thus the only assured connection
386  * between a marked mutant and future code changes is the checksum.
387  */
388 @TableName(markedMutantTable)
389 @TablePrimaryKey("checksum0")
390 struct MarkedMutantTbl {
391     /// Checksum of the mutant status the marking is related to.
392     long checksum0;
393     long checksum1;
394 
395     /// updated each analyze.
396     @ColumnName("st_id")
397     long mutationStatusId;
398 
399     /// updated each analyze.
400     @ColumnName("mut_id")
401     long mutationId;
402 
403     uint line;
404     uint column;
405     string path;
406 
407     /// The status it should always be changed to.
408     long toStatus;
409 
410     /// Time when the mutant where marked.
411     SysTime time;
412 
413     string rationale;
414 
415     string mutText;
416 }
417 
418 @TableName(schemataMutantTable)
419 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
420 @TableForeignKey("schem_id", KeyRef("schemata_fragment(id)"), KeyParam("ON DELETE CASCADE"))
421 struct SchemataMutantTable {
422     @ColumnName("st_id")
423     long statusId;
424     @ColumnName("schem_id")
425     long schemaId;
426 }
427 
428 @TableName(schemataTable)
429 struct SchemataTable {
430     long id;
431 }
432 
433 @TableName(schemataWorkListTable)
434 @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
435 struct SchemataWorkListTable {
436     long id;
437 }
438 
439 @TableName(schemataFragmentTable)
440 @TableForeignKey("schem_id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
441 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
442 struct SchemataFragmentTable {
443     long id;
444 
445     @ColumnName("schem_id")
446     long schemataId;
447 
448     @ColumnName("file_id")
449     long fileId;
450 
451     @ColumnName("order_")
452     long order;
453 
454     @ColumnParam("")
455     const(ubyte)[] text;
456 
457     @ColumnName("offset_begin")
458     uint offsetBegin;
459     @ColumnName("offset_end")
460     uint offsetEnd;
461 }
462 
463 void updateSchemaVersion(ref Miniorm db, long ver) nothrow {
464     try {
465         db.run(delete_!VersionTbl);
466         db.run(insert!VersionTbl.insert, VersionTbl(ver));
467     } catch (Exception e) {
468         logger.error(e.msg).collectException;
469     }
470 }
471 
472 long getSchemaVersion(ref Miniorm db) nothrow {
473     try {
474         auto v = db.run(select!VersionTbl);
475         return v.empty ? 0 : v.front.version_;
476     } catch (Exception e) {
477     }
478     return 0;
479 }
480 
481 void upgrade(ref Miniorm db) nothrow {
482     import d2sqlite3;
483 
484     alias upgradeFunc = void function(ref Miniorm db);
485     enum tbl = makeUpgradeTable;
486 
487     while (true) {
488         long version_ = 0;
489 
490         try {
491             version_ = getSchemaVersion(db);
492         } catch (Exception e) {
493             logger.trace(e.msg).collectException;
494         }
495 
496         if (version_ == tbl.latestSchemaVersion)
497             return;
498 
499         logger.infof("Upgrading database from %s", version_).collectException;
500 
501         if (auto f = version_ in tbl) {
502             try {
503                 db.begin;
504                 scope (success)
505                     db.commit;
506                 scope (failure)
507                     db.rollback;
508                 (*f)(db);
509                 if (version_ != 0)
510                     updateSchemaVersion(db, version_ + 1);
511             } catch (Exception e) {
512                 logger.trace(e).collectException;
513                 logger.error(e.msg).collectException;
514                 logger.warningf("Unable to upgrade a database of version %s",
515                         version_).collectException;
516                 logger.warning("This might impact the functionality. It is unwise to continue")
517                     .collectException;
518                 return;
519             }
520         } else {
521             logger.info("Upgrade successful").collectException;
522             return;
523         }
524     }
525 }
526 
527 /** If the database start it version 0, not initialized, then initialize to the
528  * latest schema version.
529  */
530 void upgradeV0(ref Miniorm db) {
531     enum tbl = makeUpgradeTable;
532 
533     db.run(buildSchema!(VersionTbl, RawSrcMetadata, FilesTbl,
534             MutationPointTbl, MutationTbl, TestCaseKilledTbl, AllTestCaseTbl,
535             MutationStatusTbl, MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl,
536             MarkedMutantTbl, SrcMetadataTable, NomutTbl, NomutDataTbl,
537             NomutDataTbl, SchemataTable, SchemataFragmentTable,
538             SchemataWorkListTable, SchemataMutantTable));
539 
540     updateSchemaVersion(db, tbl.latestSchemaVersion);
541 }
542 
543 /// 2018-04-08
544 void upgradeV1(ref Miniorm db) {
545     @TableName(testCaseTableV1)
546     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
547     static struct TestCaseKilledTblV1 {
548         ulong id;
549 
550         @ColumnName("mut_id")
551         ulong mutantId;
552 
553         /// test_case is whatever identifier the user choose.
554         @ColumnName("test_case")
555         string testCase;
556     }
557 
558     db.run(buildSchema!TestCaseKilledTblV1);
559 }
560 
561 /// 2018-04-22
562 void upgradeV2(ref Miniorm db) {
563     @TableName(filesTable)
564     static struct FilesTbl {
565         ulong id;
566 
567         @ColumnParam("")
568         string path;
569 
570         ulong checksum0;
571         ulong checksum1;
572         Language lang;
573     }
574 
575     immutable new_tbl = "new_" ~ filesTable;
576 
577     db.run(buildSchema!FilesTbl("new_"));
578     db.run(format("INSERT INTO %s (id,path,checksum0,checksum1) SELECT * FROM %s",
579             new_tbl, filesTable));
580     db.replaceTbl(new_tbl, filesTable);
581 }
582 
583 /// 2018-09-01
584 void upgradeV3(ref Miniorm db) {
585     @TableName(killedTestCaseTable)
586     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
587     struct TestCaseKilledTblV2 {
588         ulong id;
589 
590         @ColumnName("mut_id")
591         ulong mutantId;
592 
593         @ColumnParam("")
594         string name;
595 
596         // location is a filesystem location or other suitable helper for a user to
597         // locate the test.
598         @ColumnParam("")
599         string location;
600     }
601 
602     db.run(buildSchema!TestCaseKilledTblV2);
603     db.run(format("INSERT INTO %s (id,mut_id,name) SELECT * FROM %s",
604             killedTestCaseTable, testCaseTableV1));
605     db.run(format("DROP TABLE %s", testCaseTableV1));
606 
607     db.run(buildSchema!AllTestCaseTbl);
608 }
609 
610 /// 2018-09-24
611 void upgradeV4(ref Miniorm db) {
612     @TableName(killedTestCaseTable)
613     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
614     @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE"))
615     static struct TestCaseKilledTblV3 {
616         ulong id;
617 
618         @ColumnName("mut_id")
619         ulong mutantId;
620         @ColumnName("tc_id")
621         ulong testCaseId;
622 
623         // location is a filesystem location or other suitable helper for a user to
624         // locate the test.
625         @ColumnParam("")
626         string location;
627     }
628 
629     immutable new_tbl = "new_" ~ killedTestCaseTable;
630 
631     db.run(buildSchema!TestCaseKilledTblV3("new_"));
632 
633     // add all missing test cases to all_test_case
634     db.run(format("INSERT INTO %s (name) SELECT DISTINCT t1.name FROM %s t1 LEFT JOIN %s t2 ON t2.name = t1.name WHERE t2.name IS NULL",
635             allTestCaseTable, killedTestCaseTable, allTestCaseTable));
636     // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table
637     //Q: What is happening here?
638     //
639     //A: Conceptually, we select all rows from table1 and for each row we
640     //attempt to find a row in table2 with the same value for the name column.
641     //If there is no such row, we just leave the table2 portion of our result
642     //empty for that row. Then we constrain our selection by picking only those
643     //rows in the result where the matching row does not exist. Finally, We
644     //ignore all fields from our result except for the name column (the one we
645     //are sure that exists, from table1).
646     //
647     //While it may not be the most performant method possible in all cases, it
648     //should work in basically every database engine ever that attempts to
649     //implement ANSI 92 SQL
650 
651     // This do NOT WORK. The result is that that this upgrade is broken because
652     // it drops all maps between killed_test_case and mutation.
653     //db.run(format("INSERT INTO %s (id,mut_id,tc_id,location) SELECT t1.id,t1.mut_id,t2.id,t1.location FROM %s t1 INNER JOIN %s t2 ON t1.name = t2.name",
654     //        new_tbl, killedTestCaseTable, allTestCaseTable));
655 
656     db.replaceTbl(new_tbl, killedTestCaseTable);
657 }
658 
659 /** 2018-09-30
660  *
661  * This upgrade will drop all existing mutations and thus all results.
662  * It is too complex trying to upgrade and keep the results.
663  *
664  * When removing this function also remove the status field in mutation_v2_tbl.
665  */
666 void upgradeV5(ref Miniorm db) {
667     @TableName(mutationTable)
668     @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
669     @TableForeignKey("st_id", KeyRef("mutation_status(id)"))
670     @TableConstraint("unique_ UNIQUE (mp_id, kind)")
671     static struct MutationTbl {
672         ulong id;
673 
674         ulong mp_id;
675 
676         @ColumnParam("")
677         ulong st_id;
678 
679         ulong kind;
680 
681         @ColumnParam("")
682         ulong status;
683 
684         /// time in ms spent on verifying the mutant
685         @ColumnParam("")
686         ulong time;
687     }
688 
689     @TableName(mutationStatusTable)
690     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
691     static struct MutationStatusTbl {
692         ulong id;
693         ulong status;
694         ulong checksum0;
695         ulong checksum1;
696     }
697 
698     immutable new_mut_tbl = "new_" ~ mutationTable;
699     db.run(buildSchema!MutationStatusTbl);
700 
701     db.run(format("DROP TABLE %s", mutationTable));
702     db.run(buildSchema!MutationTbl);
703 
704     immutable new_files_tbl = "new_" ~ filesTable;
705     db.run(buildSchema!FilesTbl("new_"));
706     db.run(format("INSERT OR IGNORE INTO %s (id,path,checksum0,checksum1,lang) SELECT * FROM %s",
707             new_files_tbl, filesTable));
708     db.replaceTbl(new_files_tbl, filesTable);
709 }
710 
711 /// 2018-10-11
712 void upgradeV6(ref Miniorm db) {
713     @TableName(mutationStatusTable)
714     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
715     static struct MutationStatusTbl {
716         ulong id;
717         ulong status;
718         ulong time;
719         SysTime timestamp;
720         ulong checksum0;
721         ulong checksum1;
722     }
723 
724     immutable new_mut_tbl = "new_" ~ mutationTable;
725 
726     db.run(buildSchema!MutationTbl("new_"));
727 
728     db.run(format("INSERT INTO %s (id,mp_id,st_id,kind) SELECT id,mp_id,st_id,kind FROM %s",
729             new_mut_tbl, mutationTable));
730     db.replaceTbl(new_mut_tbl, mutationTable);
731 
732     immutable new_muts_tbl = "new_" ~ mutationStatusTable;
733     db.run(buildSchema!MutationStatusTbl("new_"));
734     db.run(format("INSERT INTO %s (id,status,checksum0,checksum1) SELECT id,status,checksum0,checksum1 FROM %s",
735             new_muts_tbl, mutationStatusTable));
736     db.replaceTbl(new_muts_tbl, mutationStatusTable);
737 }
738 
739 /// 2018-10-15
740 void upgradeV7(ref Miniorm db) {
741     immutable new_tbl = "new_" ~ killedTestCaseTable;
742 
743     db.run(buildSchema!TestCaseKilledTbl("new_"));
744 
745     db.run(format("INSERT INTO %s (id,st_id,tc_id,location)
746         SELECT t0.id,t1.st_id,t0.tc_id,t0.location
747         FROM %s t0, %s t1
748         WHERE
749         t0.mut_id = t1.id", new_tbl,
750             killedTestCaseTable, mutationTable));
751 
752     db.replaceTbl(new_tbl, killedTestCaseTable);
753 }
754 
755 /// 2018-10-20
756 void upgradeV8(ref Miniorm db) {
757     immutable new_tbl = "new_" ~ mutationPointTable;
758     db.run(buildSchema!MutationPointTbl("new_"));
759     db.run(format("INSERT INTO %s (id,file_id,offset_begin,offset_end,line,column)
760         SELECT t0.id,t0.file_id,t0.offset_begin,t0.offset_end,t0.line,t0.column
761         FROM %s t0",
762             new_tbl, mutationPointTable));
763 
764     db.replaceTbl(new_tbl, mutationPointTable);
765 }
766 
767 /// 2018-11-10
768 void upgradeV9(ref Miniorm db) {
769     immutable new_tbl = "new_" ~ mutationStatusTable;
770     db.run(buildSchema!MutationStatusTbl("new_"));
771     db.run(format("INSERT INTO %s (id,status,time,test_cnt,update_ts,checksum0,checksum1)
772         SELECT t0.id,t0.status,t0.time,0,t0.timestamp,t0.checksum0,t0.checksum1
773         FROM %s t0",
774             new_tbl, mutationStatusTable));
775 
776     replaceTbl(db, new_tbl, mutationStatusTable);
777 }
778 
779 /// 2018-11-25
780 void upgradeV10(ref Miniorm db) {
781     @TableName(rawSrcMetadataTable)
782     @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
783     @TableConstraint("unique_line_in_file UNIQUE (file_id, line)")
784     struct RawSrcMetadata {
785         ulong id;
786 
787         @ColumnName("file_id")
788         ulong fileId;
789 
790         @ColumnParam("")
791         uint line;
792 
793         @ColumnParam("")
794         ulong nomut;
795     }
796 
797     db.run(buildSchema!RawSrcMetadata);
798     void makeSrcMetadataView(ref Miniorm db) {
799         // check if a NOMUT is on or between the start and end of a mutant.
800         immutable src_metadata_v1_tbl = "CREATE VIEW %s
801             AS
802             SELECT
803             t0.id AS mut_id,
804             t1.id AS st_id,
805             t2.id AS mp_id,
806             t3.id AS file_id,
807             (SELECT count(*) FROM %s in_t0, %s in_t1
808              WHERE
809              in_t0.file_id = in_t1.file_id AND
810              t0.mp_id = in_t0.id AND
811              (in_t1.line BETWEEN in_t0.line AND in_t0.line_end)) AS nomut
812                 FROM %s t0, %s t1, %s t2, %s t3
813                 WHERE
814                 t0.mp_id = t2.id AND
815                 t0.st_id = t1.id AND
816                 t2.file_id = t3.id
817                 ";
818 
819         db.run(format(src_metadata_v1_tbl, srcMetadataTable, mutationPointTable, rawSrcMetadataTable,
820                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
821     }
822 
823     makeSrcMetadataView(db);
824 }
825 
826 /// 2019-04-06
827 void upgradeV11(ref Miniorm db) {
828     immutable new_tbl = "new_" ~ rawSrcMetadataTable;
829     db.run(buildSchema!RawSrcMetadata("new_"));
830     db.run(format!"INSERT INTO %s (id,file_id,line,nomut) SELECT t.id,t.file_id,t.line,t.nomut FROM %s t"(new_tbl,
831             rawSrcMetadataTable));
832     replaceTbl(db, new_tbl, rawSrcMetadataTable);
833 
834     db.run(format("DROP VIEW %s", srcMetadataTable)).collectException;
835 
836     // Associate metadata from lines with the mutation status.
837     void makeSrcMetadataView(ref Miniorm db) {
838         // check if a NOMUT is on or between the start and end of a mutant.
839         immutable src_metadata_tbl = "CREATE VIEW %s
840         AS
841         SELECT DISTINCT
842         t0.id AS mut_id,
843         t1.id AS st_id,
844         t2.id AS mp_id,
845         t3.id AS file_id,
846         (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut
847         FROM %s t0, %s t1, %s t2, %s t3
848         WHERE
849         t0.mp_id = t2.id AND
850         t0.st_id = t1.id AND
851         t2.file_id = t3.id";
852         db.run(format(src_metadata_tbl, srcMetadataTable, nomutTable,
853                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
854 
855         immutable nomut_tbl = "CREATE VIEW %s
856         AS
857         SELECT
858         t0.id mp_id,
859         t1.line line,
860         count(*) status
861         FROM %s t0, %s t1
862         WHERE
863         t0.file_id = t1.file_id AND
864         (t1.line BETWEEN t0.line AND t0.line_end)
865         GROUP BY
866         t0.id";
867         db.run(format(nomut_tbl, nomutTable, mutationPointTable, rawSrcMetadataTable));
868 
869         immutable nomut_data_tbl = "CREATE VIEW %s
870         AS
871         SELECT
872         t0.id as mut_id,
873         t0.mp_id as mp_id,
874         t1.line as line,
875         t1.tag as tag,
876         t1.comment as comment
877         FROM %s t0, %s t1, %s t2
878         WHERE
879         t0.mp_id = t2.mp_id AND
880         t1.line = t2.line";
881         db.run(format(nomut_data_tbl, nomutDataTable, mutationTable,
882                 rawSrcMetadataTable, nomutTable));
883     }
884 
885     makeSrcMetadataView(db);
886 }
887 
888 /// 2019-08-28
889 void upgradeV12(ref Miniorm db) {
890     db.run(buildSchema!(MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl));
891 }
892 
893 /// 2019-11-12
894 void upgradeV13(ref Miniorm db) {
895     @TableName(markedMutantTable)
896     @TablePrimaryKey("st_id")
897     struct MarkedMutantTbl {
898         @ColumnName("st_id")
899         long mutationStatusId;
900 
901         @ColumnName("mut_id")
902         long mutationId;
903 
904         uint line;
905 
906         uint column;
907 
908         string path;
909 
910         @ColumnName("to_status")
911         ulong toStatus;
912 
913         SysTime time;
914 
915         string rationale;
916 
917         @ColumnName("mut_text")
918         string mutText;
919     }
920 
921     db.run(buildSchema!(MarkedMutantTbl));
922 }
923 
924 /// 2020-01-12
925 void upgradeV14(ref Miniorm db) {
926     db.run(format!"DROP VIEW %s"(srcMetadataTable));
927     db.run(format!"DROP VIEW %s"(nomutTable));
928     db.run(format!"DROP VIEW %s"(nomutDataTable));
929 
930     db.run(buildSchema!(SrcMetadataTable, NomutTbl, NomutDataTbl));
931     logger.info("Re-execute analyze to update the NOMUT data");
932 }
933 
934 /// 2020-01-21
935 void upgradeV15(ref Miniorm db) {
936     // fix bug in the marked mutant table
937     db.run(format!"DROP TABLE %s"(markedMutantTable));
938     db.run(buildSchema!MarkedMutantTbl);
939     logger.info("Dropping all marked mutants because of database changes");
940 }
941 
942 /// 2020-02-12
943 void upgradeV16(ref Miniorm db) {
944     db.run(buildSchema!(SchemataFragmentTable, SchemataWorkListTable, SchemataMutantTable));
945 }
946 
947 /// 2020-02-12
948 void upgradeV17(ref Miniorm db) {
949     db.run(buildSchema!(SchemataTable));
950 }
951 
952 void replaceTbl(ref Miniorm db, string src, string dst) {
953     db.run(format("DROP TABLE %s", dst));
954     db.run(format("ALTER TABLE %s RENAME TO %s", src, dst));
955 }
956 
957 struct UpgradeTable {
958     alias UpgradeFunc = void function(ref Miniorm db);
959     UpgradeFunc[long] tbl;
960     alias tbl this;
961 
962     immutable long latestSchemaVersion;
963 }
964 
965 /** Inspects a module for functions starting with upgradeV to create a table of
966  * functions that can be used to upgrade a database.
967  */
968 UpgradeTable makeUpgradeTable() {
969     import std.algorithm : sort, startsWith;
970     import std.conv : to;
971     import std.typecons : Tuple;
972 
973     immutable prefix = "upgradeV";
974 
975     alias Module = dextool.plugin.mutate.backend.database.schema;
976 
977     // the second parameter is the database version to upgrade FROM.
978     alias UpgradeFx = Tuple!(UpgradeTable.UpgradeFunc, long);
979 
980     UpgradeFx[] upgradeFx;
981     long last_from;
982 
983     static foreach (member; __traits(allMembers, Module)) {
984         static if (member.startsWith(prefix))
985             upgradeFx ~= UpgradeFx(&__traits(getMember, Module, member),
986                     member[prefix.length .. $].to!long);
987     }
988 
989     typeof(UpgradeTable.tbl) tbl;
990     foreach (fn; upgradeFx.sort!((a, b) => a[1] < b[1])) {
991         last_from = fn[1];
992         tbl[last_from] = fn[0];
993     }
994 
995     return UpgradeTable(tbl, last_from + 1);
996 }