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 }