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 struct SchemataMutantTable { 425 @ColumnName("st_id") 426 long statusId; 427 @ColumnName("schem_id") 428 long schemaId; 429 } 430 431 @TableName(schemataTable) 432 struct SchemataTable { 433 long id; 434 435 // number of fragments the schemata consist of. 436 // used to detect if a fragment has been removed because its related file 437 // was changed. 438 long fragments; 439 440 // runtime generated constant that make it possible to "prune" old 441 // schematas automatically. it assumes that each new version of dextool may 442 // contain updates to the schematas thus the old schemats should be 443 // removed. 444 @ColumnName("version") 445 long version_; 446 } 447 448 @TableName(invalidSchemataTable) 449 @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE")) 450 struct InvalidSchemataTable { 451 long id; 452 } 453 454 @TableName(schemataFragmentTable) 455 @TableForeignKey("schem_id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE")) 456 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE")) 457 struct SchemataFragmentTable { 458 long id; 459 460 @ColumnName("schem_id") 461 long schemataId; 462 463 @ColumnName("file_id") 464 long fileId; 465 466 @ColumnName("order_") 467 long order; 468 469 @ColumnParam("") 470 const(ubyte)[] text; 471 472 @ColumnName("offset_begin") 473 uint offsetBegin; 474 @ColumnName("offset_end") 475 uint offsetEnd; 476 } 477 478 void updateSchemaVersion(ref Miniorm db, long ver) nothrow { 479 try { 480 db.run(delete_!VersionTbl); 481 db.run(insert!VersionTbl.insert, VersionTbl(ver)); 482 } catch (Exception e) { 483 logger.error(e.msg).collectException; 484 } 485 } 486 487 long getSchemaVersion(ref Miniorm db) nothrow { 488 try { 489 auto v = db.run(select!VersionTbl); 490 return v.empty ? 0 : v.front.version_; 491 } catch (Exception e) { 492 } 493 return 0; 494 } 495 496 void upgrade(ref Miniorm db) nothrow { 497 import d2sqlite3; 498 499 immutable maxIndex = 5; 500 501 alias upgradeFunc = void function(ref Miniorm db); 502 enum tbl = makeUpgradeTable; 503 504 bool hasUpdated; 505 506 bool running = true; 507 while (running) { 508 long version_ = 0; 509 510 try { 511 version_ = getSchemaVersion(db); 512 } catch (Exception e) { 513 logger.trace(e.msg).collectException; 514 } 515 516 if (version_ >= tbl.latestSchemaVersion) { 517 running = false; 518 continue; 519 } 520 521 logger.infof("Upgrading database from %s", version_).collectException; 522 523 if (!hasUpdated) 524 try { 525 // only do this once and always before any changes to the database. 526 foreach (i; 0 .. maxIndex) { 527 db.run(format!"DROP INDEX IF EXISTS i%s"(i)); 528 } 529 } catch (Exception e) { 530 logger.warning(e.msg).collectException; 531 logger.warning("Unable to drop database indexes").collectException; 532 } 533 534 if (auto f = version_ in tbl) { 535 try { 536 auto trans = db.transaction; 537 hasUpdated = true; 538 539 (*f)(db); 540 if (version_ != 0) 541 updateSchemaVersion(db, version_ + 1); 542 trans.commit; 543 } catch (Exception e) { 544 logger.trace(e).collectException; 545 logger.error(e.msg).collectException; 546 logger.warningf("Unable to upgrade a database of version %s", 547 version_).collectException; 548 logger.warning("This might impact the functionality. It is unwise to continue") 549 .collectException; 550 return; 551 } 552 } else { 553 logger.info("Upgrade successful").collectException; 554 running = false; 555 } 556 } 557 558 // add indexes assuming the lastest database schema 559 if (hasUpdated) 560 try { 561 auto trans = db.transaction; 562 int i; 563 db.run(format!"CREATE INDEX i%s ON %s(file_id)"(i++, mutationPointTable)); 564 db.run(format!"CREATE INDEX i%s ON %s(path)"(i++, filesTable)); 565 566 // improve getTestCaseMutantKills by 10x 567 db.run(format!"CREATE INDEX i%s ON %s(tc_id,st_id)"(i++, killedTestCaseTable)); 568 db.run(format!"CREATE INDEX i%s ON %s(st_id)"(i++, mutationTable)); 569 570 assert(i <= maxIndex); 571 trans.commit; 572 } catch (Exception e) { 573 logger.warning(e.msg).collectException; 574 logger.warning("Unable to create database indexes").collectException; 575 } 576 } 577 578 /** If the database start it version 0, not initialized, then initialize to the 579 * latest schema version. 580 */ 581 void upgradeV0(ref Miniorm db) { 582 enum tbl = makeUpgradeTable; 583 584 db.run(buildSchema!(VersionTbl, RawSrcMetadata, FilesTbl, 585 MutationPointTbl, MutationTbl, TestCaseKilledTbl, AllTestCaseTbl, 586 MutationStatusTbl, MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl, 587 MarkedMutantTbl, SrcMetadataTable, NomutTbl, NomutDataTbl, 588 NomutDataTbl, SchemataTable, SchemataFragmentTable, 589 SchemataMutantTable, InvalidSchemataTable)); 590 591 updateSchemaVersion(db, tbl.latestSchemaVersion); 592 } 593 594 /// 2018-04-08 595 void upgradeV1(ref Miniorm db) { 596 @TableName(testCaseTableV1) 597 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE")) 598 static struct TestCaseKilledTblV1 { 599 ulong id; 600 601 @ColumnName("mut_id") 602 ulong mutantId; 603 604 /// test_case is whatever identifier the user choose. 605 @ColumnName("test_case") 606 string testCase; 607 } 608 609 db.run(buildSchema!TestCaseKilledTblV1); 610 } 611 612 /// 2018-04-22 613 void upgradeV2(ref Miniorm db) { 614 @TableName(filesTable) 615 static struct FilesTbl { 616 ulong id; 617 618 @ColumnParam("") 619 string path; 620 621 ulong checksum0; 622 ulong checksum1; 623 Language lang; 624 } 625 626 immutable new_tbl = "new_" ~ filesTable; 627 628 db.run(buildSchema!FilesTbl("new_")); 629 db.run(format("INSERT INTO %s (id,path,checksum0,checksum1) SELECT * FROM %s", 630 new_tbl, filesTable)); 631 db.replaceTbl(new_tbl, filesTable); 632 } 633 634 /// 2018-09-01 635 void upgradeV3(ref Miniorm db) { 636 @TableName(killedTestCaseTable) 637 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE")) 638 struct TestCaseKilledTblV2 { 639 ulong id; 640 641 @ColumnName("mut_id") 642 ulong mutantId; 643 644 @ColumnParam("") 645 string name; 646 647 // location is a filesystem location or other suitable helper for a user to 648 // locate the test. 649 @ColumnParam("") 650 string location; 651 } 652 653 db.run(buildSchema!TestCaseKilledTblV2); 654 db.run(format("INSERT INTO %s (id,mut_id,name) SELECT * FROM %s", 655 killedTestCaseTable, testCaseTableV1)); 656 db.run(format("DROP TABLE %s", testCaseTableV1)); 657 658 db.run(buildSchema!AllTestCaseTbl); 659 } 660 661 /// 2018-09-24 662 void upgradeV4(ref Miniorm db) { 663 @TableName(killedTestCaseTable) 664 @TableForeignKey("mut_id", KeyRef("mutation(id)"), KeyParam("ON DELETE CASCADE")) 665 @TableForeignKey("tc_id", KeyRef("all_test_case(id)"), KeyParam("ON DELETE CASCADE")) 666 static struct TestCaseKilledTblV3 { 667 ulong id; 668 669 @ColumnName("mut_id") 670 ulong mutantId; 671 @ColumnName("tc_id") 672 ulong testCaseId; 673 674 // location is a filesystem location or other suitable helper for a user to 675 // locate the test. 676 @ColumnParam("") 677 string location; 678 } 679 680 immutable new_tbl = "new_" ~ killedTestCaseTable; 681 682 db.run(buildSchema!TestCaseKilledTblV3("new_")); 683 684 // add all missing test cases to all_test_case 685 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", 686 allTestCaseTable, killedTestCaseTable, allTestCaseTable)); 687 // https://stackoverflow.com/questions/2686254/how-to-select-all-records-from-one-table-that-do-not-exist-in-another-table 688 //Q: What is happening here? 689 // 690 //A: Conceptually, we select all rows from table1 and for each row we 691 //attempt to find a row in table2 with the same value for the name column. 692 //If there is no such row, we just leave the table2 portion of our result 693 //empty for that row. Then we constrain our selection by picking only those 694 //rows in the result where the matching row does not exist. Finally, We 695 //ignore all fields from our result except for the name column (the one we 696 //are sure that exists, from table1). 697 // 698 //While it may not be the most performant method possible in all cases, it 699 //should work in basically every database engine ever that attempts to 700 //implement ANSI 92 SQL 701 702 // This do NOT WORK. The result is that that this upgrade is broken because 703 // it drops all maps between killed_test_case and mutation. 704 //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", 705 // new_tbl, killedTestCaseTable, allTestCaseTable)); 706 707 db.replaceTbl(new_tbl, killedTestCaseTable); 708 } 709 710 /** 2018-09-30 711 * 712 * This upgrade will drop all existing mutations and thus all results. 713 * It is too complex trying to upgrade and keep the results. 714 * 715 * When removing this function also remove the status field in mutation_v2_tbl. 716 */ 717 void upgradeV5(ref Miniorm db) { 718 @TableName(mutationTable) 719 @TableForeignKey("mp_id", KeyRef("mutation_point(id)"), KeyParam("ON DELETE CASCADE")) 720 @TableForeignKey("st_id", KeyRef("mutation_status(id)")) 721 @TableConstraint("unique_ UNIQUE (mp_id, kind)") 722 static struct MutationTbl { 723 ulong id; 724 725 ulong mp_id; 726 727 @ColumnParam("") 728 ulong st_id; 729 730 ulong kind; 731 732 @ColumnParam("") 733 ulong status; 734 735 /// time in ms spent on verifying the mutant 736 @ColumnParam("") 737 ulong time; 738 } 739 740 @TableName(mutationStatusTable) 741 @TableConstraint("checksum UNIQUE (checksum0, checksum1)") 742 static struct MutationStatusTbl { 743 ulong id; 744 ulong status; 745 ulong checksum0; 746 ulong checksum1; 747 } 748 749 immutable new_mut_tbl = "new_" ~ mutationTable; 750 db.run(buildSchema!MutationStatusTbl); 751 752 db.run(format("DROP TABLE %s", mutationTable)); 753 db.run(buildSchema!MutationTbl); 754 755 immutable new_files_tbl = "new_" ~ filesTable; 756 db.run(buildSchema!FilesTbl("new_")); 757 db.run(format("INSERT OR IGNORE INTO %s (id,path,checksum0,checksum1,lang) SELECT * FROM %s", 758 new_files_tbl, filesTable)); 759 db.replaceTbl(new_files_tbl, filesTable); 760 } 761 762 /// 2018-10-11 763 void upgradeV6(ref Miniorm db) { 764 @TableName(mutationStatusTable) 765 @TableConstraint("checksum UNIQUE (checksum0, checksum1)") 766 static struct MutationStatusTbl { 767 ulong id; 768 ulong status; 769 ulong time; 770 SysTime timestamp; 771 ulong checksum0; 772 ulong checksum1; 773 } 774 775 immutable new_mut_tbl = "new_" ~ mutationTable; 776 777 db.run(buildSchema!MutationTbl("new_")); 778 779 db.run(format("INSERT INTO %s (id,mp_id,st_id,kind) SELECT id,mp_id,st_id,kind FROM %s", 780 new_mut_tbl, mutationTable)); 781 db.replaceTbl(new_mut_tbl, mutationTable); 782 783 immutable new_muts_tbl = "new_" ~ mutationStatusTable; 784 db.run(buildSchema!MutationStatusTbl("new_")); 785 db.run(format("INSERT INTO %s (id,status,checksum0,checksum1) SELECT id,status,checksum0,checksum1 FROM %s", 786 new_muts_tbl, mutationStatusTable)); 787 db.replaceTbl(new_muts_tbl, mutationStatusTable); 788 } 789 790 /// 2018-10-15 791 void upgradeV7(ref Miniorm db) { 792 immutable new_tbl = "new_" ~ killedTestCaseTable; 793 794 db.run(buildSchema!TestCaseKilledTbl("new_")); 795 796 db.run(format("INSERT INTO %s (id,st_id,tc_id,location) 797 SELECT t0.id,t1.st_id,t0.tc_id,t0.location 798 FROM %s t0, %s t1 799 WHERE 800 t0.mut_id = t1.id", new_tbl, 801 killedTestCaseTable, mutationTable)); 802 803 db.replaceTbl(new_tbl, killedTestCaseTable); 804 } 805 806 /// 2018-10-20 807 void upgradeV8(ref Miniorm db) { 808 immutable new_tbl = "new_" ~ mutationPointTable; 809 db.run(buildSchema!MutationPointTbl("new_")); 810 db.run(format("INSERT INTO %s (id,file_id,offset_begin,offset_end,line,column) 811 SELECT t0.id,t0.file_id,t0.offset_begin,t0.offset_end,t0.line,t0.column 812 FROM %s t0", 813 new_tbl, mutationPointTable)); 814 815 db.replaceTbl(new_tbl, mutationPointTable); 816 } 817 818 /// 2018-11-10 819 void upgradeV9(ref Miniorm db) { 820 immutable new_tbl = "new_" ~ mutationStatusTable; 821 db.run(buildSchema!MutationStatusTbl("new_")); 822 db.run(format("INSERT INTO %s (id,status,time,test_cnt,update_ts,checksum0,checksum1) 823 SELECT t0.id,t0.status,t0.time,0,t0.timestamp,t0.checksum0,t0.checksum1 824 FROM %s t0", 825 new_tbl, mutationStatusTable)); 826 827 replaceTbl(db, new_tbl, mutationStatusTable); 828 } 829 830 /// 2018-11-25 831 void upgradeV10(ref Miniorm db) { 832 @TableName(rawSrcMetadataTable) 833 @TableForeignKey("file_id", KeyRef("files(id)"), KeyParam("ON DELETE CASCADE")) 834 @TableConstraint("unique_line_in_file UNIQUE (file_id, line)") 835 struct RawSrcMetadata { 836 ulong id; 837 838 @ColumnName("file_id") 839 ulong fileId; 840 841 @ColumnParam("") 842 uint line; 843 844 @ColumnParam("") 845 ulong nomut; 846 } 847 848 db.run(buildSchema!RawSrcMetadata); 849 void makeSrcMetadataView(ref Miniorm db) { 850 // check if a NOMUT is on or between the start and end of a mutant. 851 immutable src_metadata_v1_tbl = "CREATE VIEW %s 852 AS 853 SELECT 854 t0.id AS mut_id, 855 t1.id AS st_id, 856 t2.id AS mp_id, 857 t3.id AS file_id, 858 (SELECT count(*) FROM %s in_t0, %s in_t1 859 WHERE 860 in_t0.file_id = in_t1.file_id AND 861 t0.mp_id = in_t0.id AND 862 (in_t1.line BETWEEN in_t0.line AND in_t0.line_end)) AS nomut 863 FROM %s t0, %s t1, %s t2, %s t3 864 WHERE 865 t0.mp_id = t2.id AND 866 t0.st_id = t1.id AND 867 t2.file_id = t3.id 868 "; 869 870 db.run(format(src_metadata_v1_tbl, srcMetadataTable, mutationPointTable, rawSrcMetadataTable, 871 mutationTable, mutationStatusTable, mutationPointTable, filesTable)); 872 } 873 874 makeSrcMetadataView(db); 875 } 876 877 /// 2019-04-06 878 void upgradeV11(ref Miniorm db) { 879 immutable new_tbl = "new_" ~ rawSrcMetadataTable; 880 db.run(buildSchema!RawSrcMetadata("new_")); 881 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, 882 rawSrcMetadataTable)); 883 replaceTbl(db, new_tbl, rawSrcMetadataTable); 884 885 db.run(format("DROP VIEW %s", srcMetadataTable)).collectException; 886 887 // Associate metadata from lines with the mutation status. 888 void makeSrcMetadataView(ref Miniorm db) { 889 // check if a NOMUT is on or between the start and end of a mutant. 890 immutable src_metadata_tbl = "CREATE VIEW %s 891 AS 892 SELECT DISTINCT 893 t0.id AS mut_id, 894 t1.id AS st_id, 895 t2.id AS mp_id, 896 t3.id AS file_id, 897 (SELECT count(*) FROM %s WHERE nomut.mp_id = t2.id) as nomut 898 FROM %s t0, %s t1, %s t2, %s t3 899 WHERE 900 t0.mp_id = t2.id AND 901 t0.st_id = t1.id AND 902 t2.file_id = t3.id"; 903 db.run(format(src_metadata_tbl, srcMetadataTable, nomutTable, 904 mutationTable, mutationStatusTable, mutationPointTable, filesTable)); 905 906 immutable nomut_tbl = "CREATE VIEW %s 907 AS 908 SELECT 909 t0.id mp_id, 910 t1.line line, 911 count(*) status 912 FROM %s t0, %s t1 913 WHERE 914 t0.file_id = t1.file_id AND 915 (t1.line BETWEEN t0.line AND t0.line_end) 916 GROUP BY 917 t0.id"; 918 db.run(format(nomut_tbl, nomutTable, mutationPointTable, rawSrcMetadataTable)); 919 920 immutable nomut_data_tbl = "CREATE VIEW %s 921 AS 922 SELECT 923 t0.id as mut_id, 924 t0.mp_id as mp_id, 925 t1.line as line, 926 t1.tag as tag, 927 t1.comment as comment 928 FROM %s t0, %s t1, %s t2 929 WHERE 930 t0.mp_id = t2.mp_id AND 931 t1.line = t2.line"; 932 db.run(format(nomut_data_tbl, nomutDataTable, mutationTable, 933 rawSrcMetadataTable, nomutTable)); 934 } 935 936 makeSrcMetadataView(db); 937 } 938 939 /// 2019-08-28 940 void upgradeV12(ref Miniorm db) { 941 db.run(buildSchema!(MutantTimeoutCtxTbl, MutantTimeoutWorklistTbl)); 942 } 943 944 /// 2019-11-12 945 void upgradeV13(ref Miniorm db) { 946 @TableName(markedMutantTable) 947 @TablePrimaryKey("st_id") 948 struct MarkedMutantTbl { 949 @ColumnName("st_id") 950 long mutationStatusId; 951 952 @ColumnName("mut_id") 953 long mutationId; 954 955 uint line; 956 957 uint column; 958 959 string path; 960 961 @ColumnName("to_status") 962 ulong toStatus; 963 964 SysTime time; 965 966 string rationale; 967 968 @ColumnName("mut_text") 969 string mutText; 970 } 971 972 db.run(buildSchema!(MarkedMutantTbl)); 973 } 974 975 /// 2020-01-12 976 void upgradeV14(ref Miniorm db) { 977 db.run(format!"DROP VIEW %s"(srcMetadataTable)); 978 db.run(format!"DROP VIEW %s"(nomutTable)); 979 db.run(format!"DROP VIEW %s"(nomutDataTable)); 980 981 db.run(buildSchema!(SrcMetadataTable, NomutTbl, NomutDataTbl)); 982 logger.info("Re-execute analyze to update the NOMUT data"); 983 } 984 985 /// 2020-01-21 986 void upgradeV15(ref Miniorm db) { 987 // fix bug in the marked mutant table 988 db.run(format!"DROP TABLE %s"(markedMutantTable)); 989 db.run(buildSchema!MarkedMutantTbl); 990 logger.info("Dropping all marked mutants because of database changes"); 991 } 992 993 /// 2020-02-12 994 void upgradeV16(ref Miniorm db) { 995 @TableName(schemataWorkListTable) 996 @TableForeignKey("id", KeyRef("schemata(id)"), KeyParam("ON DELETE CASCADE")) 997 static struct SchemataWorkListTable { 998 long id; 999 } 1000 1001 @TableName(schemataMutantTable) 1002 @TableForeignKey("st_id", KeyRef("mutation_status(id)"), KeyParam("ON DELETE CASCADE")) 1003 @TableForeignKey("schem_id", KeyRef("schemata_fragment(id)"), KeyParam("ON DELETE CASCADE")) 1004 static struct SchemataMutantTable { 1005 @ColumnName("st_id") 1006 long statusId; 1007 @ColumnName("schem_id") 1008 long schemaId; 1009 } 1010 1011 db.run(buildSchema!(SchemataFragmentTable, SchemataWorkListTable, SchemataMutantTable)); 1012 } 1013 1014 /// 2020-02-12 1015 void upgradeV17(ref Miniorm db) { 1016 @TableName(schemataTable) 1017 static struct SchemataTable { 1018 long id; 1019 } 1020 1021 db.run(buildSchema!(SchemataTable)); 1022 } 1023 1024 /// 2020-03-21 1025 void upgradeV18(ref Miniorm db) { 1026 // this force an old database to add indexes 1027 } 1028 1029 /// 2020-04-01 1030 void upgradeV19(ref Miniorm db) { 1031 db.run(format!"DROP TABLE %s"(schemataWorkListTable)); 1032 db.run(format!"DROP TABLE %s"(schemataTable)); 1033 db.run(format!"DROP TABLE %s"(schemataMutantTable)); 1034 1035 db.run(buildSchema!(SchemataTable, SchemataMutantTable, InvalidSchemataTable)); 1036 } 1037 1038 void replaceTbl(ref Miniorm db, string src, string dst) { 1039 db.run(format("DROP TABLE %s", dst)); 1040 db.run(format("ALTER TABLE %s RENAME TO %s", src, dst)); 1041 } 1042 1043 struct UpgradeTable { 1044 alias UpgradeFunc = void function(ref Miniorm db); 1045 UpgradeFunc[long] tbl; 1046 alias tbl this; 1047 1048 immutable long latestSchemaVersion; 1049 } 1050 1051 /** Inspects a module for functions starting with upgradeV to create a table of 1052 * functions that can be used to upgrade a database. 1053 */ 1054 UpgradeTable makeUpgradeTable() { 1055 import std.algorithm : sort, startsWith; 1056 import std.conv : to; 1057 import std.typecons : Tuple; 1058 1059 immutable prefix = "upgradeV"; 1060 1061 alias Module = dextool.plugin.mutate.backend.database.schema; 1062 1063 // the second parameter is the database version to upgrade FROM. 1064 alias UpgradeFx = Tuple!(UpgradeTable.UpgradeFunc, long); 1065 1066 UpgradeFx[] upgradeFx; 1067 long last_from; 1068 1069 static foreach (member; __traits(allMembers, Module)) { 1070 static if (member.startsWith(prefix)) 1071 upgradeFx ~= UpgradeFx(&__traits(getMember, Module, member), 1072 member[prefix.length .. $].to!long); 1073 } 1074 1075 typeof(UpgradeTable.tbl) tbl; 1076 foreach (fn; upgradeFx.sort!((a, b) => a[1] < b[1])) { 1077 last_from = fn[1]; 1078 tbl[last_from] = fn[0]; 1079 } 1080 1081 return UpgradeTable(tbl, last_from + 1); 1082 }