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