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 # Style 13 A database schema upgrade path shall have a comment stating what date it was added. 14 Each change to the database schema must have an equal upgrade added. 15 16 # Sqlite3 17 From the sqlite3 manual $(LINK https://www.sqlite.org/datatype3.html): 18 Each value stored in an SQLite database (or manipulated by the database 19 engine) has one of the following storage classes: 20 21 NULL. The value is a NULL value. 22 23 INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes 24 depending on the magnitude of the value. 25 26 REAL. The value is a floating point value, stored as an 8-byte IEEE floating 27 point number. 28 29 TEXT. The value is a text string, stored using the database encoding (UTF-8, 30 UTF-16BE or UTF-16LE). 31 32 BLOB. The value is a blob of data, stored exactly as it was input. 33 34 A storage class is more general than a datatype. The INTEGER storage class, for 35 example, includes 6 different integer datatypes of different lengths. This 36 makes a difference on disk. But as soon as INTEGER values are read off of disk 37 and into memory for processing, they are converted to the most general datatype 38 (8-byte signed integer). And so for the most part, "storage class" is 39 indistinguishable from "datatype" and the two terms can be used 40 interchangeably. 41 */ 42 module dextool.plugin.mutate.backend.database.schema; 43 44 import std.exception : collectException; 45 import logger = std.experimental.logger; 46 47 import d2sqlite3 : sqlDatabase = Database; 48 49 immutable latestSchemaVersion = 2; 50 immutable schemaVersionTable = "schema_version"; 51 immutable filesTable = "files"; 52 immutable mutationPointTable = "mutation_point"; 53 immutable mutationTable = "mutation"; 54 immutable testCaseTable = "test_case"; 55 56 /** Initialize or open an existing database. 57 * 58 * Params: 59 * p = path where to initialize a new database or open an existing 60 * 61 * Returns: an open sqlite3 database object. 62 */ 63 sqlDatabase* initializeDB(const string p) @trusted 64 in { 65 assert(p.length != 0); 66 } 67 do { 68 import d2sqlite3; 69 70 sqlDatabase* db; 71 72 void setPragmas(sqlDatabase* db) { 73 // dfmt off 74 auto pragmas = [ 75 // required for foreign keys with cascade to work 76 "PRAGMA foreign_keys=ON;", 77 // use two worker threads. Should improve performance a bit without having an adverse effect. 78 // this should probably be user configurable. 79 "PRAGMA threads = 2;", 80 ]; 81 // dfmt on 82 83 foreach (p; pragmas) { 84 db.run(p); 85 } 86 } 87 88 try { 89 db = new sqlDatabase(p, SQLITE_OPEN_READWRITE); 90 upgrade(*db); 91 } 92 catch (Exception e) { 93 logger.trace(e.msg); 94 logger.trace("Initializing a new sqlite3 database"); 95 } 96 97 if (db is null) { 98 db = new sqlDatabase(p, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE); 99 initializeTables(*db); 100 } 101 102 setPragmas(db); 103 104 return db; 105 } 106 107 private: 108 109 immutable version_tbl = "CREATE TABLE %s ( 110 version INTEGER NOT NULL 111 )"; 112 113 // checksum is 128bit. Using a integer to better represent and search for them 114 // in queries. 115 immutable files_tbl = "CREATE TABLE %s ( 116 id INTEGER PRIMARY KEY, 117 path TEXT NOT NULL, 118 checksum0 INTEGER NOT NULL, 119 checksum1 INTEGER NOT NULL 120 )"; 121 122 // line start from zero 123 // there shall never exist two mutations points for the same file+offset. 124 immutable mutation_point_tbl = "CREATE TABLE %s ( 125 id INTEGER PRIMARY KEY, 126 file_id INTEGER NOT NULL, 127 offset_begin INTEGER NOT NULL, 128 offset_end INTEGER NOT NULL, 129 line INTEGER, 130 column INTEGER, 131 FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE, 132 CONSTRAINT file_offset UNIQUE (file_id, offset_begin, offset_end) 133 )"; 134 135 // time in ms spent on verifying the mutant 136 immutable mutation_tbl = "CREATE TABLE %s ( 137 id INTEGER PRIMARY KEY, 138 mp_id INTEGER NOT NULL, 139 kind INTEGER NOT NULL, 140 status INTEGER NOT NULL, 141 time INTEGER, 142 FOREIGN KEY(mp_id) REFERENCES mutation_point(id) ON DELETE CASCADE 143 )"; 144 145 // test_case is whatever identifier the user choose. 146 // this could use an intermediate adapter table to normalise the test_case data 147 // but I chose not to do that because it makes it harder to add test cases and 148 // do a cleanup. 149 immutable test_case_tbl = "CREATE TABLE %s ( 150 id INTEGER PRIMARY KEY, 151 mut_id INTEGER NOT NULL, 152 test_case TEXT NOT NULL, 153 FOREIGN KEY(mut_id) REFERENCES mutation(id) ON DELETE CASCADE 154 )"; 155 156 void initializeTables(ref sqlDatabase db) { 157 import std.format : format; 158 159 db.run(format(version_tbl, schemaVersionTable)); 160 updateSchemaVersion(db, latestSchemaVersion); 161 162 db.run(format(files_tbl, filesTable)); 163 db.run(format(mutation_point_tbl, mutationPointTable)); 164 db.run(format(mutation_tbl, mutationTable)); 165 db.run(format(test_case_tbl, testCaseTable)); 166 } 167 168 void updateSchemaVersion(ref sqlDatabase db, long ver) { 169 import std.format : format; 170 171 try { 172 auto stmt = db.prepare(format("DELETE FROM %s", schemaVersionTable)); 173 stmt.execute; 174 175 stmt = db.prepare(format("INSERT INTO %s (version) VALUES(:ver)", schemaVersionTable)); 176 stmt.bind(":ver", ver); 177 stmt.execute; 178 } 179 catch (Exception e) { 180 logger.error(e.msg).collectException; 181 } 182 } 183 184 long getSchemaVersion(ref sqlDatabase db) { 185 enum version_q = "SELECT version FROM " ~ schemaVersionTable; 186 auto stmt = db.prepare(version_q); 187 auto res = stmt.execute; 188 if (!res.empty) 189 return res.oneValue!long; 190 return 0; 191 } 192 193 void upgrade(ref sqlDatabase db) nothrow { 194 import d2sqlite3; 195 196 alias upgradeFunc = void function(ref sqlDatabase db); 197 upgradeFunc[long] tbl; 198 199 tbl[0] = &upgradeV0; 200 tbl[1] = &upgradeV1; 201 202 while (true) { 203 long version_ = 0; 204 205 try { 206 version_ = getSchemaVersion(db); 207 } 208 catch (Exception e) { 209 logger.trace(e.msg).collectException; 210 } 211 212 if (version_ == latestSchemaVersion) 213 return; 214 215 logger.infof("Upgrading database from %s", version_).collectException; 216 217 if (auto f = version_ in tbl) { 218 try { 219 db.begin; 220 scope (success) 221 db.commit; 222 scope (failure) 223 db.rollback; 224 (*f)(db); 225 } 226 catch (Exception e) { 227 logger.error(e.msg).collectException; 228 logger.warningf("Unable to upgrade a database of version %s", 229 version_).collectException; 230 logger.warning("This might impact the functionality. It is unwise to continue") 231 .collectException; 232 return; 233 } 234 } else { 235 logger.info("Upgrade successful").collectException; 236 return; 237 } 238 } 239 } 240 241 /// 2018-04-07 242 void upgradeV0(ref sqlDatabase db) { 243 import std.format : format; 244 245 db.run(format(version_tbl, schemaVersionTable)); 246 updateSchemaVersion(db, 1); 247 } 248 249 /// 2018-04-08 250 void upgradeV1(ref sqlDatabase db) { 251 import std.format : format; 252 253 db.run(format(test_case_tbl, testCaseTable)); 254 updateSchemaVersion(db, 2); 255 }