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