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