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