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