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