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 }