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