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