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