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