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 depFileTable = "dependency_file";
68 immutable depRootTable = "rel_dependency_root";
69 immutable dextoolVersionTable = "dextool_version";
70 immutable filesTable = "files";
71 immutable killedTestCaseTable = "killed_test_case";
72 immutable markedMutantTable = "marked_mutant";
73 immutable mutantTimeoutCtxTable = "mutant_timeout_ctx";
74 immutable mutantTimeoutWorklistTable = "mutant_timeout_worklist";
75 immutable mutantWorklistTable = "mutant_worklist";
76 immutable mutationPointTable = "mutation_point";
77 immutable mutationScoreHistoryTable = "mutation_score_history";
78 immutable mutationStatusTable = "mutation_status";
79 immutable mutationTable = "mutation";
80 immutable nomutDataTable = "nomut_data";
81 immutable nomutTable = "nomut";
82 immutable rawSrcMetadataTable = "raw_src_metadata";
83 immutable runtimeHistoryTable = "test_cmd_runtime_history";
84 immutable schemaVersionTable = "schema_version";
85 immutable schemataFragmentTable = "schemata_fragment";
86 immutable schemataMutantTable = "schemata_mutant";
87 immutable schemataTable = "schemata";
88 immutable schemataUsedTable = "schemata_used";
89 immutable srcCovInfoTable = "src_cov_info";
90 immutable srcCovTable = "src_cov_instr";
91 immutable srcCovTimeStampTable = "src_cov_timestamp";
92 immutable srcMetadataTable = "src_metadata";
93 immutable testFilesTable = "test_files";
94 
95 private immutable invalidSchemataTable = "invalid_schemata";
96 private immutable schemataWorkListTable = "schemata_worklist";
97 private immutable testCaseTableV1 = "test_case";
98 
99 /** Initialize or open an existing database.
100  *
101  * Params:
102  *  p = path where to initialize a new database or open an existing
103  *
104  * Returns: an open sqlite3 database object.
105  */
106 Miniorm initializeDB(const string p) @trusted
107 in {
108     assert(p.length != 0);
109 }
110 do {
111     import std.parallelism : totalCPUs;
112     import d2sqlite3 : SQLITE_OPEN_CREATE, SQLITE_OPEN_READWRITE;
113 
114     static void setPragmas(ref SqlDatabase db) {
115         // dfmt off
116         auto pragmas = [
117             // required for foreign keys with cascade to work
118             "PRAGMA foreign_keys=ON;",
119         ];
120         // dfmt on
121 
122         foreach (p; pragmas) {
123             db.run(p);
124         }
125     }
126 
127     SqlDatabase sqliteDb;
128 
129     try {
130         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE);
131     } catch (Exception e) {
132         logger.trace(e.msg);
133         logger.trace("Initializing a new sqlite3 database");
134         sqliteDb = SqlDatabase(p, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
135     }
136 
137     auto db = Miniorm(sqliteDb);
138 
139     // TODO: remove all key off in upgrade schemas.
140     db.run("PRAGMA foreign_keys=OFF;");
141     try {
142         upgrade(db);
143     } catch (Exception e) {
144         logger.error("Unable to upgrade the database to the latest schema");
145         throw e;
146     }
147     setPragmas(sqliteDb);
148 
149     return db;
150 }
151 
152 package:
153 
154 // metadata about mutants that occur on a line extracted from the source code.
155 // It is intended to further refined.
156 // nomut = if the line should ignore mutants.
157 // tag = a user defined tag for a NOMUT.
158 // comment = a user defined comment.
159 @TableName(rawSrcMetadataTable)
160 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
161 @TableConstraint("unique_line_in_file UNIQUE (file_id, line)")
162 struct RawSrcMetadata {
163     long id;
164 
165     @ColumnName("file_id")
166     long fileId;
167 
168     @ColumnParam("")
169     uint line;
170 
171     @ColumnParam("")
172     long nomut;
173 
174     @ColumnParam("")
175     string tag;
176 
177     @ColumnParam("")
178     string comment;
179 }
180 
181 @TableName(srcMetadataTable)
182 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
183 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
184 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
185 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
186 struct SrcMetadataTable {
187     @ColumnName("mut_id")
188     long mutationId;
189 
190     @ColumnName("st_id")
191     long mutationStatusId;
192 
193     @ColumnName("mp_id")
194     long mutationPointId;
195 
196     @ColumnName("file_id")
197     long fileId;
198 
199     @ColumnName("nomut")
200     long nomutCount;
201 }
202 
203 // Reconstruct the nomut table in Miniorm.
204 @TableName(nomutTable)
205 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
206 struct NomutTbl {
207     @ColumnName("mp_id")
208     long mutationPointId;
209 
210     long line;
211 
212     /// != 0 when a nomut is tagged on the line.
213     long status;
214 }
215 
216 @TableName(nomutDataTable)
217 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
218 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
219 struct NomutDataTbl {
220     @ColumnName("mut_id")
221     long mutationId;
222 
223     @ColumnName("mp_id")
224     long mutationPointId;
225 
226     long line;
227 
228     @ColumnParam("")
229     string tag;
230 
231     @ColumnParam("")
232     string comment;
233 }
234 
235 @TableName(schemaVersionTable)
236 struct VersionTbl {
237     @ColumnName("version")
238     long version_;
239 }
240 
241 @TableName(dextoolVersionTable)
242 struct DextoolVersionTable {
243     /// checksum is 64bit.
244     long checksum;
245 }
246 
247 @TableName(filesTable)
248 @TableConstraint("unique_ UNIQUE (path)")
249 struct FilesTbl {
250     long id;
251 
252     string path;
253 
254     /// checksum is 128bit.
255     long checksum0;
256     long checksum1;
257     Language lang;
258 
259     @ColumnName("timestamp")
260     SysTime timeStamp;
261 
262     /// True if the file is a root.
263     bool root;
264 }
265 
266 @TableName(testFilesTable)
267 @TableConstraint("unique_ UNIQUE (path)")
268 struct TestFilesTable {
269     long id;
270 
271     string path;
272 
273     /// checksum is 128bit.
274     long checksum0;
275     long checksum1;
276 
277     /// Last time a change to the test file where detected.
278     @ColumnName("timestamp")
279     SysTime timeStamp;
280 }
281 
282 /// there shall never exist two mutations points for the same file+offset.
283 @TableName(mutationPointTable)
284 @TableConstraint("file_offset UNIQUE (file_id, offset_begin, offset_end)")
285 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
286 struct MutationPointTbl {
287     long id;
288     long file_id;
289     uint offset_begin;
290     uint offset_end;
291 
292     /// line start from zero
293     @ColumnParam("")
294     uint line;
295     @ColumnParam("")
296     uint column;
297 
298     @ColumnParam("")
299     uint line_end;
300 
301     @ColumnParam("")
302     uint column_end;
303 }
304 
305 @TableName(mutationTable)
306 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
307 @TableForeignKey("st_id", KeyRef("mutation_status(id)"))
308 @TableConstraint("unique_ UNIQUE (mp_id, kind)")
309 struct MutationTbl {
310     long id;
311 
312     long mp_id;
313 
314     @ColumnParam("")
315     long st_id;
316 
317     long kind;
318 }
319 
320 /**
321  * This could use an intermediate adapter table to normalise the test_case data
322  * but I chose not to do that because it makes it harder to add test cases and
323  * do a cleanup.
324  */
325 @TableName(killedTestCaseTable)
326 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
327 @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE"))
328 @TableConstraint("unique_ UNIQUE (st_id, tc_id)")
329 struct TestCaseKilledTbl {
330     long id;
331 
332     @ColumnName("st_id")
333     long mutationStatusId;
334     @ColumnName("tc_id")
335     long testCaseId;
336 
337     // location is a filesystem location or other suitable helper for a user to
338     // locate the test.
339     @ColumnParam("")
340     string location;
341 }
342 
343 /**
344  * Track all test cases that has been found by the test suite output analyzer.
345  * Useful to find test cases that has never killed any mutant.
346  * name should match test_case_killed_v2_tbl
347  * TODO: name should be the primary key. on a conflict a counter should be updated.
348  */
349 @TableName(allTestCaseTable)
350 @TableConstraint("unique_ UNIQUE (name)")
351 struct AllTestCaseTbl {
352     long id;
353     string name;
354 }
355 
356 /**
357  * the status of a mutant. if it is killed or otherwise.
358  * multiple mutation operators can result in the same change of the source
359  * code. By coupling the mutant status to the checksum of the source code
360  * change it means that two mutations that have the same checksum will
361  * "cooperate".
362  * TODO: change the checksum to being NOT NULL in the future. Can't for now
363  * when migrating to schema version 5->6.
364  * compile_time_ms = time it took to compile the program for the mutant
365  * test_time_ms = time it took to run the test suite
366  * updated_ts = is when the status where last updated. Seconds at UTC+0.
367  * added_ts = when the mutant where added to the system. UTC+0.
368  */
369 @TableName(mutationStatusTable)
370 @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
371 struct MutationStatusTbl {
372     long id;
373     long status;
374     @ColumnName("exit_code")
375     int exitCode;
376 
377     @ColumnName("compile_time_ms")
378     long compileTimeMs;
379 
380     @ColumnName("test_time_ms")
381     long testTimeMs;
382 
383     @ColumnParam("")
384     @ColumnName("update_ts")
385     SysTime updated;
386 
387     @ColumnParam("")
388     @ColumnName("added_ts")
389     SysTime added;
390 
391     long checksum0;
392     long checksum1;
393 
394     /// Priority of the mutant used when testing.
395     long prio;
396 }
397 
398 /** Mutants that should be tested.
399  */
400 @TableName(mutantWorklistTable)
401 @TableForeignKey("id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
402 struct MutantWorklistTbl {
403     long id;
404     long prio;
405 }
406 
407 /** Timeout mutants that are re-tested until none of them change status from
408  * timeout.
409  */
410 @TableName(mutantTimeoutWorklistTable)
411 @TableForeignKey("id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
412 struct MutantTimeoutWorklistTbl {
413     long id;
414 }
415 
416 /** The defaults for the schema is the state that the state machine start in.
417  *
418  * This mean that if there are nothing in the database then `.init` is the correct starting point.
419  */
420 @TableName(mutantTimeoutCtxTable)
421 struct MutantTimeoutCtxTbl {
422     /// What iteration the timeout testing is at.
423     long iter;
424 
425     /// Last count of the mutants in the worklist that where in the timeout state.
426     long worklistCount;
427 
428     enum State {
429         init_,
430         running,
431         done
432     }
433 
434     /// State of the timeout algorithm.
435     State state;
436 }
437 
438 /** The lower 64bit of the checksum should be good enough as the primary key.
439  * By doing it this way it is easier to update a marked mutant without
440  * "peeking" in the database ("insert or update").
441  *
442  * Both `st_id` and `mut_id` are values that sqlite can reuse between analyzes
443  * if they have been previously removed thus the only assured connection
444  * between a marked mutant and future code changes is the checksum.
445  */
446 @TableName(markedMutantTable)
447 @TablePrimaryKey("checksum0")
448 struct MarkedMutantTbl {
449     /// Checksum of the mutant status the marking is related to.
450     long checksum0;
451     long checksum1;
452 
453     /// updated each analyze.
454     @ColumnName("st_id")
455     long mutationStatusId;
456 
457     /// updated each analyze.
458     @ColumnName("mut_id")
459     long mutationId;
460 
461     uint line;
462     uint column;
463     string path;
464 
465     /// The status it should always be changed to.
466     long toStatus;
467 
468     /// Time when the mutant where marked.
469     SysTime time;
470 
471     string rationale;
472 
473     string mutText;
474 }
475 
476 @TableName(schemataMutantTable)
477 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
478 @TableForeignKey("schem_id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
479 @TableConstraint("unique_ UNIQUE (st_id, schem_id)")
480 struct SchemataMutantTable {
481     @ColumnName("st_id")
482     long statusId;
483     @ColumnName("schem_id")
484     long schemaId;
485 }
486 
487 @TableName(schemataTable)
488 struct SchemataTable {
489     long id;
490 
491     // number of fragments the schemata consist of.
492     // used to detect if a fragment has been removed because its related file
493     // was changed.
494     long fragments;
495 }
496 
497 @TableName(schemataUsedTable)
498 @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
499 struct SchemataUsedTable {
500     long id;
501 }
502 
503 @TableName(schemataFragmentTable)
504 @TableForeignKey("schem_id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
505 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
506 struct SchemataFragmentTable {
507     long id;
508 
509     @ColumnName("schem_id")
510     long schemataId;
511 
512     @ColumnName("file_id")
513     long fileId;
514 
515     @ColumnName("order_")
516     long order;
517 
518     @ColumnParam("")
519     const(ubyte)[] text;
520 
521     @ColumnName("offset_begin")
522     uint offsetBegin;
523     @ColumnName("offset_end")
524     uint offsetEnd;
525 }
526 
527 /** The runtime of the test commands.
528  *
529  * By storing the data it reduces the need to run the test suite multiple times
530  * to get the minimum.
531  */
532 @TableName(runtimeHistoryTable)
533 struct RuntimeHistoryTable {
534     long id;
535 
536     /// when the measurement was taken.
537     @ColumnName("time")
538     SysTime timeStamp;
539 
540     @ColumnName("time_ms")
541     long timeMs;
542 }
543 
544 @TableName(mutationScoreHistoryTable)
545 struct MutationScoreHistoryTable {
546     long id;
547 
548     /// when the measurement was taken.
549     @ColumnName("time")
550     SysTime timeStamp;
551 
552     double score;
553 }
554 
555 /** All functions that has been discovered in the source code.
556  * The `offset_begin` is where instrumentation points can be injected.
557  */
558 @TableName(srcCovTable)
559 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
560 @TableConstraint("file_offset UNIQUE (file_id, begin, end)")
561 struct CoverageCodeRegionTable {
562     long id;
563 
564     @ColumnName("file_id")
565     long fileId;
566 
567     /// the region in the files, in bytes.
568     uint begin;
569     uint end;
570 }
571 
572 /** Each coverage region that has a valid status has an entry in this table.
573  * It do mean that there can be region that do not exist in this table. That
574  * mean that something went wrong when gathering the data.
575  */
576 @TableName(srcCovInfoTable)
577 @TableForeignKey("id", KeyRef("src_cov_info(id)"), KeyParam("ON DELETE CASCADE"))
578 struct CoverageInfoTable {
579     long id;
580 
581     /// True if the region has been visited.
582     bool status;
583 }
584 
585 /// When the coverage information was gathered.
586 @TableName(srcCovTimeStampTable)
587 struct CoverageTimeTtampTable {
588     long id;
589 
590     @ColumnName("timestamp")
591     SysTime timeStamp;
592 }
593 
594 /** Files that roots are dependent on. They do not need to contain mutants.
595  */
596 @TableName(depFileTable)
597 @TableConstraint("unique_ UNIQUE (file)")
598 struct DependencyFileTable {
599     long id;
600 
601     string file;
602 
603     /// checksum is 128bit.
604     long checksum0;
605     long checksum1;
606 }
607 
608 @TableName(depRootTable)
609 @TableForeignKey("dep_id", KeyRef("dependency_file(id)"), KeyParam("ON DELETE CASCADE"))
610 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
611 @TableConstraint("unique_ UNIQUE (dep_id, file_id)")
612 struct DependencyRootTable {
613     @ColumnName("dep_id")
614     long depFileId;
615 
616     @ColumnName("file_id")
617     long fileId;
618 }
619 
620 void updateSchemaVersion(ref Miniorm db, long ver) nothrow {
621     try {
622         db.run(delete_!VersionTbl);
623         db.run(insert!VersionTbl.insert, VersionTbl(ver));
624     } catch (Exception e) {
625         logger.error(e.msg).collectException;
626     }
627 }
628 
629 long getSchemaVersion(ref Miniorm db) nothrow {
630     try {
631         auto v = db.run(select!VersionTbl);
632         return v.empty ? 0 : v.front.version_;
633     } catch (Exception e) {
634     }
635     return 0;
636 }
637 
638 void upgrade(ref Miniorm db) {
639     import d2sqlite3;
640 
641     immutable maxIndex = 5;
642 
643     alias upgradeFunc = void function(ref Miniorm db);
644     auto tbl = makeUpgradeTable;
645 
646     bool hasUpdated;
647 
648     bool running = true;
649     while (running) {
650         long version_ = 0;
651 
652         try {
653             version_ = getSchemaVersion(db);
654         } catch (Exception e) {
655             logger.trace(e.msg).collectException;
656             // try again
657             continue;
658         }
659 
660         if (version_ >= tbl.latestSchemaVersion) {
661             running = false;
662             continue;
663         }
664 
665         logger.infof("Upgrading database from %s", version_).collectException;
666 
667         if (!hasUpdated)
668             try {
669                 // only do this once and always before any changes to the database.
670                 foreach (i; 0 .. maxIndex) {
671                     db.run(format!"DROP INDEX IF EXISTS i%s"(i));
672                 }
673             } catch (Exception e) {
674                 logger.warning(e.msg).collectException;
675                 logger.warning("Unable to drop database indexes").collectException;
676             }
677 
678         if (auto f = version_ in tbl) {
679             try {
680                 auto trans = db.transaction;
681                 hasUpdated = true;
682 
683                 (*f)(db);
684                 if (version_ != 0)
685                     updateSchemaVersion(db, version_ + 1);
686                 trans.commit;
687             } catch (Exception e) {
688                 logger.trace(e).collectException;
689                 logger.error(e.msg).collectException;
690                 logger.warningf("Unable to upgrade a database of version %s",
691                         version_).collectException;
692                 logger.warning("This might impact the functionality. It is unwise to continue")
693                     .collectException;
694                 return;
695             }
696         } else {
697             logger.info("Upgrade successful").collectException;
698             running = false;
699         }
700     }
701 
702     // add indexes assuming the lastest database schema
703     if (hasUpdated)
704         try {
705             auto trans = db.transaction;
706             int i;
707             db.run(format!"CREATE INDEX i%s ON %s(file_id)"(i++, mutationPointTable));
708             db.run(format!"CREATE INDEX i%s ON %s(path)"(i++, filesTable));
709             db.run(format!"CREATE INDEX i%s ON %s(path)"(i++, testFilesTable));
710 
711             // improve getTestCaseMutantKills by 10x
712             db.run(format!"CREATE INDEX i%s ON %s(tc_id,st_id)"(i++, killedTestCaseTable));
713             db.run(format!"CREATE INDEX i%s ON %s(st_id)"(i++, mutationTable));
714 
715             assert(i <= maxIndex);
716             trans.commit;
717         } catch (Exception e) {
718             logger.warning(e.msg).collectException;
719             logger.warning("Unable to create database indexes").collectException;
720         }
721 }
722 
723 /** If the database start it version 0, not initialized, then initialize to the
724  * latest schema version.
725  */
726 void upgradeV0(ref Miniorm db) {
727     auto tbl = makeUpgradeTable;
728 
729     db.run(buildSchema!(VersionTbl, RawSrcMetadata, FilesTbl,
730             MutationPointTbl, MutationTbl, TestCaseKilledTbl, AllTestCaseTbl,
731             MutationStatusTbl, MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl,
732             MarkedMutantTbl, SrcMetadataTable, NomutTbl, NomutDataTbl,
733             NomutDataTbl, SchemataTable, SchemataFragmentTable,
734             SchemataMutantTable,
735             SchemataUsedTable, MutantWorklistTbl, RuntimeHistoryTable,
736             MutationScoreHistoryTable, TestFilesTable, CoverageCodeRegionTable, CoverageInfoTable,
737             CoverageTimeTtampTable, DependencyFileTable, DependencyRootTable, DextoolVersionTable));
738 
739     updateSchemaVersion(db, tbl.latestSchemaVersion);
740 }
741 
742 /// 2018-04-08
743 void upgradeV1(ref Miniorm db) {
744     @TableName(allTestCaseTable)
745     struct AllTestCaseTbl {
746         long id;
747 
748         @ColumnParam("")
749         string name;
750     }
751 
752     @TableName(testCaseTableV1)
753     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
754     static struct TestCaseKilledTblV1 {
755         ulong id;
756 
757         @ColumnName("mut_id")
758         ulong mutantId;
759 
760         /// test_case is whatever identifier the user choose.
761         @ColumnName("test_case")
762         string testCase;
763     }
764 
765     db.run(buildSchema!(TestCaseKilledTblV1, AllTestCaseTbl));
766 }
767 
768 /// 2018-04-22
769 void upgradeV2(ref Miniorm db) {
770     @TableName(filesTable)
771     static struct FilesTbl {
772         ulong id;
773 
774         @ColumnParam("")
775         string path;
776 
777         ulong checksum0;
778         ulong checksum1;
779         Language lang;
780     }
781 
782     immutable newTbl = "new_" ~ filesTable;
783 
784     db.run(buildSchema!FilesTbl("new_"));
785     db.run(format("INSERT INTO %s (id,path,checksum0,checksum1) SELECT * FROM %s",
786             newTbl, filesTable));
787     db.replaceTbl(newTbl, filesTable);
788 }
789 
790 /// 2018-09-01
791 void upgradeV3(ref Miniorm db) {
792     @TableName(killedTestCaseTable)
793     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
794     struct TestCaseKilledTblV2 {
795         ulong id;
796 
797         @ColumnName("mut_id")
798         ulong mutantId;
799 
800         @ColumnParam("")
801         string name;
802 
803         // location is a filesystem location or other suitable helper for a user to
804         // locate the test.
805         @ColumnParam("")
806         string location;
807     }
808 
809     db.run(buildSchema!TestCaseKilledTblV2);
810     db.run(format("INSERT INTO %s (id,mut_id,name) SELECT * FROM %s",
811             killedTestCaseTable, testCaseTableV1));
812     db.run(format("DROP TABLE %s", testCaseTableV1));
813 
814     db.run(buildSchema!AllTestCaseTbl);
815 }
816 
817 /// 2018-09-24
818 void upgradeV4(ref Miniorm db) {
819     @TableName(killedTestCaseTable)
820     @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE"))
821     @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE"))
822     static struct TestCaseKilledTblV3 {
823         ulong id;
824 
825         @ColumnName("mut_id")
826         ulong mutantId;
827         @ColumnName("tc_id")
828         ulong testCaseId;
829 
830         // location is a filesystem location or other suitable helper for a user to
831         // locate the test.
832         @ColumnParam("")
833         string location;
834     }
835 
836     immutable newTbl = "new_" ~ killedTestCaseTable;
837 
838     db.run(buildSchema!TestCaseKilledTblV3("new_"));
839 
840     // add all missing test cases to all_test_case
841     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",
842             allTestCaseTable, killedTestCaseTable, allTestCaseTable));
843     // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table
844     //Q: What is happening here?
845     //
846     //A: Conceptually, we select all rows from table1 and for each row we
847     //attempt to find a row in table2 with the same value for the name column.
848     //If there is no such row, we just leave the table2 portion of our result
849     //empty for that row. Then we constrain our selection by picking only those
850     //rows in the result where the matching row does not exist. Finally, We
851     //ignore all fields from our result except for the name column (the one we
852     //are sure that exists, from table1).
853     //
854     //While it may not be the most performant method possible in all cases, it
855     //should work in basically every database engine ever that attempts to
856     //implement ANSI 92 SQL
857 
858     // This do NOT WORK. The result is that that this upgrade is broken because
859     // it drops all maps between killed_test_case and mutation.
860     //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",
861     //        newTbl, killedTestCaseTable, allTestCaseTable));
862 
863     db.replaceTbl(newTbl, killedTestCaseTable);
864 }
865 
866 /** 2018-09-30
867  *
868  * This upgrade will drop all existing mutations and thus all results.
869  * It is too complex trying to upgrade and keep the results.
870  *
871  * When removing this function also remove the status field in mutation_v2_tbl.
872  */
873 void upgradeV5(ref Miniorm db) {
874     @TableName(filesTable)
875     @TableConstraint("unique_ UNIQUE (path)")
876     struct FilesTbl {
877         long id;
878 
879         @ColumnParam("")
880         string path;
881 
882         /// checksum is 128bit.
883         long checksum0;
884         long checksum1;
885         Language lang;
886     }
887 
888     @TableName(mutationTable)
889     @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE"))
890     @TableForeignKey("st_id", KeyRef("mutation_status(id)"))
891     @TableConstraint("unique_ UNIQUE (mp_id, kind)")
892     static struct MutationTbl {
893         ulong id;
894 
895         ulong mp_id;
896 
897         @ColumnParam("")
898         ulong st_id;
899 
900         ulong kind;
901 
902         @ColumnParam("")
903         ulong status;
904 
905         /// time in ms spent on verifying the mutant
906         @ColumnParam("")
907         ulong time;
908     }
909 
910     @TableName(mutationStatusTable)
911     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
912     static struct MutationStatusTbl {
913         ulong id;
914         ulong status;
915         ulong checksum0;
916         ulong checksum1;
917     }
918 
919     immutable new_mut_tbl = "new_" ~ mutationTable;
920     db.run(buildSchema!MutationStatusTbl);
921 
922     db.run(format("DROP TABLE %s", mutationTable));
923     db.run(buildSchema!MutationTbl);
924 
925     immutable newFilesTbl = "new_" ~ filesTable;
926     db.run(buildSchema!FilesTbl("new_"));
927     db.run(format("INSERT OR IGNORE INTO %s (id,path,checksum0,checksum1,lang) SELECT * FROM %s",
928             newFilesTbl, filesTable));
929     db.replaceTbl(newFilesTbl, filesTable);
930 }
931 
932 /// 2018-10-11
933 void upgradeV6(ref Miniorm db) {
934     @TableName(mutationStatusTable)
935     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
936     static struct MutationStatusTbl {
937         ulong id;
938         ulong status;
939         ulong time;
940         SysTime timestamp;
941         ulong checksum0;
942         ulong checksum1;
943     }
944 
945     immutable new_mut_tbl = "new_" ~ mutationTable;
946 
947     db.run(buildSchema!MutationTbl("new_"));
948 
949     db.run(format("INSERT INTO %s (id,mp_id,st_id,kind) SELECT id,mp_id,st_id,kind FROM %s",
950             new_mut_tbl, mutationTable));
951     db.replaceTbl(new_mut_tbl, mutationTable);
952 
953     immutable new_muts_tbl = "new_" ~ mutationStatusTable;
954     db.run(buildSchema!MutationStatusTbl("new_"));
955     db.run(format("INSERT INTO %s (id,status,checksum0,checksum1) SELECT id,status,checksum0,checksum1 FROM %s",
956             new_muts_tbl, mutationStatusTable));
957     db.replaceTbl(new_muts_tbl, mutationStatusTable);
958 }
959 
960 /// 2018-10-15
961 void upgradeV7(ref Miniorm db) {
962     @TableName(killedTestCaseTable)
963     @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
964     @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE"))
965     struct TestCaseKilledTbl {
966         long id;
967 
968         @ColumnName("st_id")
969         long mutationStatusId;
970         @ColumnName("tc_id")
971         long testCaseId;
972 
973         // location is a filesystem location or other suitable helper for a user to
974         // locate the test.
975         @ColumnParam("")
976         string location;
977     }
978 
979     immutable newTbl = "new_" ~ killedTestCaseTable;
980 
981     db.run(buildSchema!TestCaseKilledTbl("new_"));
982 
983     db.run(format("INSERT INTO %s (id,st_id,tc_id,location)
984         SELECT t0.id,t1.st_id,t0.tc_id,t0.location
985         FROM %s t0, %s t1
986         WHERE
987         t0.mut_id = t1.id", newTbl,
988             killedTestCaseTable, mutationTable));
989 
990     db.replaceTbl(newTbl, killedTestCaseTable);
991 }
992 
993 /// 2018-10-20
994 void upgradeV8(ref Miniorm db) {
995     immutable newTbl = "new_" ~ mutationPointTable;
996     db.run(buildSchema!MutationPointTbl("new_"));
997     db.run(format("INSERT INTO %s (id,file_id,offset_begin,offset_end,line,column)
998         SELECT t0.id,t0.file_id,t0.offset_begin,t0.offset_end,t0.line,t0.column
999         FROM %s t0",
1000             newTbl, mutationPointTable));
1001 
1002     db.replaceTbl(newTbl, mutationPointTable);
1003 }
1004 
1005 /// 2018-11-10
1006 void upgradeV9(ref Miniorm db) {
1007     @TableName(mutationStatusTable)
1008     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
1009     struct MutationStatusTbl {
1010         long id;
1011         long status;
1012 
1013         @ColumnParam("")
1014         long time;
1015 
1016         @ColumnName("test_cnt")
1017         long testCnt;
1018 
1019         @ColumnParam("")
1020         @ColumnName("update_ts")
1021         SysTime updated;
1022 
1023         @ColumnParam("")
1024         @ColumnName("added_ts")
1025         SysTime added;
1026 
1027         long checksum0;
1028         long checksum1;
1029     }
1030 
1031     immutable newTbl = "new_" ~ mutationStatusTable;
1032     db.run(buildSchema!MutationStatusTbl("new_"));
1033     db.run(format("INSERT INTO %s (id,status,time,test_cnt,update_ts,checksum0,checksum1)
1034         SELECT t0.id,t0.status,t0.time,0,t0.timestamp,t0.checksum0,t0.checksum1
1035         FROM %s t0",
1036             newTbl, mutationStatusTable));
1037 
1038     replaceTbl(db, newTbl, mutationStatusTable);
1039 }
1040 
1041 /// 2018-11-25
1042 void upgradeV10(ref Miniorm db) {
1043     @TableName(rawSrcMetadataTable)
1044     @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE"))
1045     @TableConstraint("unique_line_in_file UNIQUE (file_id, line)")
1046     struct RawSrcMetadata {
1047         ulong id;
1048 
1049         @ColumnName("file_id")
1050         ulong fileId;
1051 
1052         @ColumnParam("")
1053         uint line;
1054 
1055         @ColumnParam("")
1056         ulong nomut;
1057     }
1058 
1059     db.run(buildSchema!RawSrcMetadata);
1060     void makeSrcMetadataView(ref Miniorm db) {
1061         // check if a NOMUT is on or between the start and end of a mutant.
1062         immutable src_metadata_v1_tbl = "CREATE VIEW %s
1063             AS
1064             SELECT
1065             t0.id AS mut_id,
1066             t1.id AS st_id,
1067             t2.id AS mp_id,
1068             t3.id AS file_id,
1069             (SELECT count(*) FROM %s in_t0, %s in_t1
1070              WHERE
1071              in_t0.file_id = in_t1.file_id AND
1072              t0.mp_id = in_t0.id AND
1073              (in_t1.line BETWEEN in_t0.line AND in_t0.line_end)) AS nomut
1074                 FROM %s t0, %s t1, %s t2, %s t3
1075                 WHERE
1076                 t0.mp_id = t2.id AND
1077                 t0.st_id = t1.id AND
1078                 t2.file_id = t3.id
1079                 ";
1080 
1081         db.run(format(src_metadata_v1_tbl, srcMetadataTable, mutationPointTable, rawSrcMetadataTable,
1082                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
1083     }
1084 
1085     makeSrcMetadataView(db);
1086 }
1087 
1088 /// 2019-04-06
1089 void upgradeV11(ref Miniorm db) {
1090     immutable newTbl = "new_" ~ rawSrcMetadataTable;
1091     db.run(buildSchema!RawSrcMetadata("new_"));
1092     db.run(format!"INSERT INTO %s (id,file_id,line,nomut) SELECT t.id,t.file_id,t.line,t.nomut FROM %s t"(newTbl,
1093             rawSrcMetadataTable));
1094     replaceTbl(db, newTbl, rawSrcMetadataTable);
1095 
1096     db.run(format("DROP VIEW %s", srcMetadataTable)).collectException;
1097 
1098     // Associate metadata from lines with the mutation status.
1099     void makeSrcMetadataView(ref Miniorm db) {
1100         // check if a NOMUT is on or between the start and end of a mutant.
1101         immutable src_metadata_tbl = "CREATE VIEW %s
1102         AS
1103         SELECT DISTINCT
1104         t0.id AS mut_id,
1105         t1.id AS st_id,
1106         t2.id AS mp_id,
1107         t3.id AS file_id,
1108         (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut
1109         FROM %s t0, %s t1, %s t2, %s t3
1110         WHERE
1111         t0.mp_id = t2.id AND
1112         t0.st_id = t1.id AND
1113         t2.file_id = t3.id";
1114         db.run(format(src_metadata_tbl, srcMetadataTable, nomutTable,
1115                 mutationTable, mutationStatusTable, mutationPointTable, filesTable));
1116 
1117         immutable nomut_tbl = "CREATE VIEW %s
1118         AS
1119         SELECT
1120         t0.id mp_id,
1121         t1.line line,
1122         count(*) status
1123         FROM %s t0, %s t1
1124         WHERE
1125         t0.file_id = t1.file_id AND
1126         (t1.line BETWEEN t0.line AND t0.line_end)
1127         GROUP BY
1128         t0.id";
1129         db.run(format(nomut_tbl, nomutTable, mutationPointTable, rawSrcMetadataTable));
1130 
1131         immutable nomut_data_tbl = "CREATE VIEW %s
1132         AS
1133         SELECT
1134         t0.id as mut_id,
1135         t0.mp_id as mp_id,
1136         t1.line as line,
1137         t1.tag as tag,
1138         t1.comment as comment
1139         FROM %s t0, %s t1, %s t2
1140         WHERE
1141         t0.mp_id = t2.mp_id AND
1142         t1.line = t2.line";
1143         db.run(format(nomut_data_tbl, nomutDataTable, mutationTable,
1144                 rawSrcMetadataTable, nomutTable));
1145     }
1146 
1147     makeSrcMetadataView(db);
1148 }
1149 
1150 /// 2019-08-28
1151 void upgradeV12(ref Miniorm db) {
1152     db.run(buildSchema!(MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl));
1153 }
1154 
1155 /// 2019-11-12
1156 void upgradeV13(ref Miniorm db) {
1157     @TableName(markedMutantTable)
1158     @TablePrimaryKey("st_id")
1159     struct MarkedMutantTbl {
1160         @ColumnName("st_id")
1161         long mutationStatusId;
1162 
1163         @ColumnName("mut_id")
1164         long mutationId;
1165 
1166         uint line;
1167 
1168         uint column;
1169 
1170         string path;
1171 
1172         @ColumnName("to_status")
1173         ulong toStatus;
1174 
1175         SysTime time;
1176 
1177         string rationale;
1178 
1179         @ColumnName("mut_text")
1180         string mutText;
1181     }
1182 
1183     db.run(buildSchema!(MarkedMutantTbl));
1184 }
1185 
1186 /// 2020-01-12
1187 void upgradeV14(ref Miniorm db) {
1188     db.run(format!"DROP VIEW %s"(srcMetadataTable));
1189     db.run(format!"DROP VIEW %s"(nomutTable));
1190     db.run(format!"DROP VIEW %s"(nomutDataTable));
1191 
1192     db.run(buildSchema!(SrcMetadataTable, NomutTbl, NomutDataTbl));
1193     logger.info("Re-execute analyze to update the NOMUT data");
1194 }
1195 
1196 /// 2020-01-21
1197 void upgradeV15(ref Miniorm db) {
1198     // fix bug in the marked mutant table
1199     db.run(format!"DROP TABLE %s"(markedMutantTable));
1200     db.run(buildSchema!MarkedMutantTbl);
1201     logger.info("Dropping all marked mutants because of database changes");
1202 }
1203 
1204 /// 2020-02-12
1205 void upgradeV16(ref Miniorm db) {
1206     @TableName(schemataWorkListTable)
1207     @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
1208     static struct SchemataWorkListTable {
1209         long id;
1210     }
1211 
1212     @TableName(schemataMutantTable)
1213     @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
1214     @TableForeignKey("schem_id", KeyRef("schemata_fragment(id)"), KeyParam("ON DELETE CASCADE"))
1215     static struct SchemataMutantTable {
1216         @ColumnName("st_id")
1217         long statusId;
1218         @ColumnName("schem_id")
1219         long schemaId;
1220     }
1221 
1222     db.run(buildSchema!(SchemataFragmentTable, SchemataWorkListTable, SchemataMutantTable));
1223 }
1224 
1225 /// 2020-02-12
1226 void upgradeV17(ref Miniorm db) {
1227     @TableName(schemataTable)
1228     static struct SchemataTable {
1229         long id;
1230     }
1231 
1232     db.run(buildSchema!(SchemataTable));
1233 }
1234 
1235 /// 2020-03-21
1236 void upgradeV18(ref Miniorm db) {
1237     // this force an old database to add indexes
1238 }
1239 
1240 /// 2020-04-01
1241 void upgradeV19(ref Miniorm db) {
1242     db.run("DROP TABLE " ~ schemataWorkListTable);
1243     db.run("DROP TABLE " ~ schemataTable);
1244     db.run("DROP TABLE " ~ schemataMutantTable);
1245 
1246     @TableName(invalidSchemataTable)
1247     @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE"))
1248     struct InvalidSchemataTable {
1249         long id;
1250     }
1251 
1252     @TableName(schemataTable)
1253     struct SchemataTable {
1254         long id;
1255 
1256         // number of fragments the schemata consist of.
1257         // used to detect if a fragment has been removed because its related file
1258         // was changed.
1259         long fragments;
1260 
1261         // runtime generated constant that make it possible to "prune" old
1262         // schematas automatically. it assumes that each new version of dextool may
1263         // contain updates to the schematas thus the old schemats should be
1264         // removed.
1265         @ColumnName("version")
1266         long version_;
1267     }
1268 
1269     db.run(buildSchema!(SchemataTable, SchemataMutantTable, InvalidSchemataTable));
1270 }
1271 
1272 /// 2020-06-01
1273 void upgradeV20(ref Miniorm db) {
1274     db.run("DROP TABLE " ~ schemataMutantTable);
1275     db.run(buildSchema!(SchemataMutantTable));
1276 }
1277 
1278 /// 2020-11-28
1279 void upgradeV21(ref Miniorm db) {
1280     db.run("DROP TABLE " ~ invalidSchemataTable);
1281     db.run(buildSchema!(SchemataUsedTable));
1282 }
1283 
1284 /// 2020-11-28
1285 void upgradeV22(ref Miniorm db) {
1286     @TableName(mutantWorklistTable)
1287     @TableForeignKey("id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE"))
1288     struct MutantWorklistTbl {
1289         long id;
1290     }
1291 
1292     db.run(buildSchema!(MutantWorklistTbl));
1293 }
1294 
1295 /// 2020-12-06
1296 void upgradeV23(ref Miniorm db) {
1297     db.run(buildSchema!(RuntimeHistoryTable));
1298 }
1299 
1300 /// 2020-12-06
1301 void upgradeV24(ref Miniorm db) {
1302     db.run(buildSchema!(MutationScoreHistoryTable));
1303 }
1304 
1305 /// 2020-12-25
1306 void upgradeV25(ref Miniorm db) {
1307     import std.traits : EnumMembers;
1308     import dextool.plugin.mutate.backend.type : Mutation;
1309 
1310     @TableName(mutationStatusTable)
1311     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
1312     struct MutationStatusTbl {
1313         long id;
1314         long status;
1315         @ColumnName("exit_code")
1316         int exitCode;
1317 
1318         @ColumnParam("")
1319         long time;
1320 
1321         @ColumnName("test_cnt")
1322         long testCnt;
1323 
1324         @ColumnParam("")
1325         @ColumnName("update_ts")
1326         SysTime updated;
1327 
1328         @ColumnParam("")
1329         @ColumnName("added_ts")
1330         SysTime added;
1331 
1332         long checksum0;
1333         long checksum1;
1334     }
1335 
1336     immutable newTbl = "new_" ~ mutationStatusTable;
1337     db.run(buildSchema!MutationStatusTbl("new_"));
1338 
1339     auto stmt = db.prepare(format(
1340             "INSERT INTO %s (id,status,exit_code,time,test_cnt,update_ts,added_ts,checksum0,checksum1)
1341         SELECT t.id,t.status,:ecode,t.time,t.test_cnt,t.update_ts,t.added_ts,t.checksum0,t.checksum1
1342         FROM %s t WHERE t.status = :status",
1343             newTbl, mutationStatusTable));
1344 
1345     foreach (st; [EnumMembers!(Mutation.Status)]) {
1346         stmt.get.bind(":ecode", (st == Mutation.Status.killed) ? 1 : 0);
1347         stmt.get.bind(":status", cast(long) st);
1348         stmt.get.execute;
1349         stmt.get.reset;
1350     }
1351 
1352     replaceTbl(db, newTbl, mutationStatusTable);
1353 }
1354 
1355 /// 2020-12-25
1356 void upgradeV26(ref Miniorm db) {
1357     immutable newTbl = "new_" ~ killedTestCaseTable;
1358     db.run(buildSchema!TestCaseKilledTbl("new_"));
1359 
1360     db.run(format("INSERT OR IGNORE INTO %s (id, st_id, tc_id, location)
1361         SELECT t.id, t.st_id, t.tc_id, t.location
1362         FROM %s t",
1363             newTbl, killedTestCaseTable));
1364     replaceTbl(db, newTbl, killedTestCaseTable);
1365 }
1366 
1367 /// 2020-12-25
1368 void upgradeV27(ref Miniorm db) {
1369     immutable newTbl = "new_" ~ allTestCaseTable;
1370     db.run(buildSchema!AllTestCaseTbl("new_"));
1371 
1372     db.run(format("INSERT OR IGNORE INTO %s (id, name)
1373         SELECT t.id, t.name
1374         FROM %s t", newTbl, allTestCaseTable));
1375     replaceTbl(db, newTbl, allTestCaseTable);
1376 }
1377 
1378 /// 2020-12-25
1379 void upgradeV28(ref Miniorm db) {
1380     import dextool.plugin.mutate.backend.type : Mutation;
1381 
1382     @TableName(mutationStatusTable)
1383     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
1384     struct MutationStatusTbl {
1385         long id;
1386         long status;
1387         @ColumnName("exit_code")
1388         int exitCode;
1389 
1390         @ColumnName("compile_time_ms")
1391         long compileTimeMs;
1392 
1393         @ColumnName("test_time_ms")
1394         long testTimeMs;
1395 
1396         @ColumnName("test_cnt")
1397         long testCnt;
1398 
1399         @ColumnParam("")
1400         @ColumnName("update_ts")
1401         SysTime updated;
1402 
1403         @ColumnParam("")
1404         @ColumnName("added_ts")
1405         SysTime added;
1406 
1407         long checksum0;
1408         long checksum1;
1409     }
1410 
1411     immutable newTbl = "new_" ~ mutationStatusTable;
1412     db.run(buildSchema!MutationStatusTbl("new_"));
1413 
1414     db.run(format("INSERT INTO %s (id,status,exit_code,compile_time_ms,test_time_ms,test_cnt,update_ts,added_ts,checksum0,checksum1)
1415         SELECT t.id,t.status,t.exit_code,0,t.time,t.test_cnt,t.update_ts,t.added_ts,t.checksum0,t.checksum1
1416         FROM %s t WHERE t.time NOT NULL", newTbl, mutationStatusTable));
1417 
1418     db.run(format("INSERT INTO %s (id,status,exit_code,compile_time_ms,test_time_ms,test_cnt,update_ts,added_ts,checksum0,checksum1)
1419         SELECT t.id,t.status,t.exit_code,0,0,t.test_cnt,t.update_ts,t.added_ts,t.checksum0,t.checksum1
1420         FROM %s t WHERE t.time IS NULL", newTbl, mutationStatusTable));
1421 
1422     replaceTbl(db, newTbl, mutationStatusTable);
1423 }
1424 
1425 /// 2020-12-27
1426 void upgradeV29(ref Miniorm db) {
1427     db.run(buildSchema!(TestFilesTable));
1428 }
1429 
1430 /// 2020-12-29
1431 void upgradeV30(ref Miniorm db) {
1432     import std.datetime : Clock;
1433     import miniorm : toSqliteDateTime;
1434 
1435     @TableName(filesTable)
1436     @TableConstraint("unique_ UNIQUE (path)")
1437     struct FilesTbl {
1438         long id;
1439 
1440         string path;
1441 
1442         /// checksum is 128bit.
1443         long checksum0;
1444         long checksum1;
1445         Language lang;
1446 
1447         @ColumnName("timestamp")
1448         SysTime timeStamp;
1449     }
1450 
1451     immutable newFilesTbl = "new_" ~ filesTable;
1452     db.run(buildSchema!FilesTbl("new_"));
1453     auto stmt = db.prepare(format("INSERT OR IGNORE INTO %s (id,path,checksum0,checksum1,lang,timestamp)
1454                   SELECT id,path,checksum0,checksum1,lang,:time FROM %s",
1455             newFilesTbl, filesTable));
1456     stmt.get.bind(":time", Clock.currTime.toSqliteDateTime);
1457     stmt.get.execute;
1458     db.replaceTbl(newFilesTbl, filesTable);
1459 }
1460 
1461 /// 2020-12-29
1462 void upgradeV31(ref Miniorm db) {
1463     db.run(buildSchema!(CoverageCodeRegionTable, CoverageInfoTable, CoverageTimeTtampTable));
1464 }
1465 
1466 /// 2021-01-02
1467 void upgradeV32(ref Miniorm db) {
1468     immutable newFilesTbl = "new_" ~ filesTable;
1469     db.run(buildSchema!FilesTbl("new_"));
1470     db.run(format("INSERT OR IGNORE INTO %s (id,path,checksum0,checksum1,lang,timestamp,root)
1471                   SELECT id,path,checksum0,checksum1,lang,timestamp,0 FROM %s",
1472             newFilesTbl, filesTable));
1473     db.replaceTbl(newFilesTbl, filesTable);
1474 }
1475 
1476 /// 2021-01-15
1477 void upgradeV33(ref Miniorm db) {
1478     db.run(buildSchema!(DependencyFileTable, DependencyRootTable));
1479 
1480     // add all existing files as being dependent on each other.
1481     // this forces, if any one of them changes, all to be re-analyzed.
1482     db.run(format("INSERT OR IGNORE INTO %1$s (file,checksum0,checksum1)
1483                   SELECT path,0,0 FROM %2$s",
1484             depFileTable, filesTable));
1485     db.run(format("INSERT OR IGNORE INTO %1$s (dep_id,file_id)
1486                   SELECT t0.id,t1.id FROM %2$s t0, %3$s t1", depRootTable,
1487             depFileTable, filesTable));
1488 }
1489 
1490 /// 2021-03-14
1491 void upgradeV34(ref Miniorm db) {
1492     immutable newTbl = "new_" ~ mutantWorklistTable;
1493     db.run(buildSchema!MutantWorklistTbl("new_"));
1494     db.run(format("INSERT INTO %1$s (id,prio) SELECT id,0 FROM %2$s", newTbl, mutantWorklistTable));
1495     db.replaceTbl(newTbl, mutantWorklistTable);
1496 }
1497 
1498 /// 2021-03-28
1499 void upgradeV35(ref Miniorm db) {
1500     @TableName(mutationStatusTable)
1501     @TableConstraint("checksum UNIQUE (checksum0, checksum1)")
1502     struct MutationStatusTbl {
1503         long id;
1504         long status;
1505         @ColumnName("exit_code")
1506         int exitCode;
1507 
1508         @ColumnName("compile_time_ms")
1509         long compileTimeMs;
1510 
1511         @ColumnName("test_time_ms")
1512         long testTimeMs;
1513 
1514         @ColumnName("test_cnt")
1515         long testCnt;
1516 
1517         @ColumnParam("")
1518         @ColumnName("update_ts")
1519         SysTime updated;
1520 
1521         @ColumnParam("")
1522         @ColumnName("added_ts")
1523         SysTime added;
1524 
1525         long checksum0;
1526         long checksum1;
1527 
1528         /// Priority of the mutant used when testing.
1529         long prio;
1530     }
1531 
1532     immutable newTbl = "new_" ~ mutationStatusTable;
1533     db.run(buildSchema!MutationStatusTbl("new_"));
1534 
1535     db.run(format("INSERT INTO %s (id,status,exit_code,compile_time_ms,test_time_ms,test_cnt,update_ts,added_ts,checksum0,checksum1,prio)
1536         SELECT t.id,t.status,t.exit_code,t.compile_time_ms,t.test_time_ms,t.test_cnt,t.update_ts,t.added_ts,t.checksum0,t.checksum1,0
1537         FROM %s t", newTbl, mutationStatusTable));
1538     replaceTbl(db, newTbl, mutationStatusTable);
1539 }
1540 
1541 /// 2021-03-29
1542 void upgradeV36(ref Miniorm db) {
1543     import dextool.plugin.mutate.backend.type : Mutation;
1544 
1545     immutable newTbl = "new_" ~ mutationStatusTable;
1546     db.run(buildSchema!MutationStatusTbl("new_"));
1547 
1548     db.run(format(
1549             "INSERT INTO %s (id,status,exit_code,compile_time_ms,test_time_ms,update_ts,added_ts,checksum0,checksum1,prio)
1550         SELECT t.id,t.status,t.exit_code,t.compile_time_ms,t.test_time_ms,t.update_ts,t.added_ts,t.checksum0,t.checksum1,t.prio
1551         FROM %s t",
1552             newTbl, mutationStatusTable));
1553     replaceTbl(db, newTbl, mutationStatusTable);
1554 }
1555 
1556 // 2021-04-18
1557 void upgradeV37(ref Miniorm db) {
1558     db.run(format("DELETE FROM %s", schemataTable));
1559     db.run(buildSchema!(DextoolVersionTable, SchemataTable));
1560 }
1561 
1562 // 2021-04-19
1563 void upgradeV38(ref Miniorm db) {
1564     db.run(format("DROP TABLE %s", schemataTable));
1565     db.run(buildSchema!(SchemataTable));
1566 }
1567 
1568 // 2021-04-23
1569 void upgradeV39(ref Miniorm db) {
1570     // this is just to force a re-measure of the test suite.
1571     db.run(format("DELETE FROM %s", runtimeHistoryTable));
1572 }
1573 
1574 // 2021-05-06
1575 void upgradeV40(ref Miniorm db) {
1576     // force an upgrade because after this release all scheman will be zipped.
1577     db.run(format("DELETE FROM %s", schemataFragmentTable));
1578     db.run(format("DELETE FROM %s", schemataMutantTable));
1579     db.run(format("DELETE FROM %s", schemataTable));
1580     db.run(format("DELETE FROM %s", schemataUsedTable));
1581 }
1582 
1583 void replaceTbl(ref Miniorm db, string src, string dst) {
1584     db.run("DROP TABLE " ~ dst);
1585     db.run(format("ALTER TABLE %s RENAME TO %s", src, dst));
1586 }
1587 
1588 struct UpgradeTable {
1589     alias UpgradeFunc = void function(ref Miniorm db);
1590     UpgradeFunc[long] tbl;
1591     alias tbl this;
1592 
1593     immutable long latestSchemaVersion;
1594 }
1595 
1596 /** Inspects a module for functions starting with upgradeV to create a table of
1597  * functions that can be used to upgrade a database.
1598  */
1599 UpgradeTable makeUpgradeTable() {
1600     import std.algorithm : sort, startsWith;
1601     import std.conv : to;
1602     import std.typecons : Tuple;
1603 
1604     immutable prefix = "upgradeV";
1605 
1606     alias Module = dextool.plugin.mutate.backend.database.schema;
1607 
1608     // the second parameter is the database version to upgrade FROM.
1609     alias UpgradeFx = Tuple!(UpgradeTable.UpgradeFunc, long);
1610 
1611     UpgradeFx[] upgradeFx;
1612     long last_from;
1613 
1614     static foreach (member; __traits(allMembers, Module)) {
1615         static if (member.startsWith(prefix))
1616             upgradeFx ~= UpgradeFx(&__traits(getMember, Module, member),
1617                     member[prefix.length .. $].to!long);
1618     }
1619 
1620     typeof(UpgradeTable.tbl) tbl;
1621     foreach (fn; upgradeFx.sort!((a, b) => a[1] < b[1])) {
1622         last_from = fn[1];
1623         tbl[last_from] = fn[0];
1624     }
1625 
1626     return UpgradeTable(tbl, last_from + 1);
1627 }