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