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