Line data Source code
1 : #include "DB.h"
2 :
3 : #include "../utilities/FangLogging.h"
4 :
5 : #include <QFile>
6 : #include <QDir>
7 : #include <QStringList>
8 : #include <QStandardPaths>
9 : #include <QSqlError>
10 :
11 : DB* DB::_instance = nullptr;
12 :
13 5 : DB::DB(QObject *parent) :
14 5 : QObject(parent)
15 : {
16 5 : init();
17 5 : }
18 :
19 286 : DB *DB::instance()
20 : {
21 286 : if (_instance == nullptr) {
22 5 : _instance = new DB();
23 : }
24 :
25 286 : return _instance;
26 : }
27 :
28 5 : void DB::initForTesting(QSqlDatabase testDb)
29 : {
30 5 : _db = testDb;
31 :
32 : // Enable foreign keys.
33 5 : executeSimpleQuery("PRAGMA foreign_keys = ON");
34 :
35 : // Create schema for testing.
36 5 : upgrade();
37 5 : }
38 :
39 5 : void DB::init()
40 : {
41 : // Find out where our data storage should go.
42 5 : QStringList list = QStandardPaths::standardLocations(QStandardPaths::ConfigLocation);
43 5 : if (list.size() == 0) {
44 0 : qCDebug(logDb) << "Qt couldn't find a data folder!";
45 0 : return;
46 : }
47 :
48 : // Create data dir if it doesn't exist.
49 5 : QString sDir = list.at(0);
50 5 : QDir dataDirectory(sDir);
51 5 : if (!dataDirectory.exists()) {
52 0 : qCDebug(logDb) << "Need to create config path: " << sDir;
53 0 : dataDirectory.mkpath(sDir);
54 : }
55 :
56 : // Open (or create) our SQLite database.
57 5 : QFile dbFile(sDir + "/fang.sqlite");
58 5 : _db = QSqlDatabase::addDatabase("QSQLITE");
59 5 : _db.setDatabaseName(dbFile.fileName());
60 10 : qCDebug(logDb) << "DB filename: " << dbFile.fileName();
61 5 : if (!_db.open()) {
62 0 : qCDebug(logDb) << "Could not create database.";
63 0 : return;
64 : }
65 :
66 : // Set database mode.
67 5 : executeSimpleQuery("PRAGMA journal_mode = WAL");
68 5 : executeSimpleQuery("PRAGMA synchronous = 1");
69 :
70 : // Enable foreign keys.
71 5 : executeSimpleQuery("PRAGMA foreign_keys = ON");
72 :
73 : // Create/upgrade schema.
74 5 : upgrade();
75 5 : }
76 :
77 22 : bool DB::executeSimpleQuery(QString query)
78 : {
79 22 : QSqlQuery q(db());
80 :
81 : // Set database mode.
82 22 : if (!q.exec(query)) {
83 2 : qCDebug(logDb) << q.lastError().text();
84 :
85 1 : return false;
86 : }
87 :
88 21 : return true;
89 22 : }
90 :
91 10 : int DB::getSchemaVersion()
92 : {
93 10 : QSqlQuery q(db());
94 10 : q.exec("PRAGMA user_version");
95 :
96 : // SQLite should auto-init value to zero, but just in case...
97 10 : if (!q.next()) {
98 0 : return 0;
99 : }
100 :
101 10 : return q.value(0).toInt();
102 10 : }
103 :
104 20 : void DB::setSchemaVersion(int version)
105 : {
106 : // QSql can't handle PRAGMAs in prepared statements,
107 : // so this may look a little whack.
108 20 : QString statement;
109 20 : QTextStream output(&statement);
110 20 : output << "PRAGMA user_version = " << version;
111 :
112 20 : QSqlQuery q(db());
113 20 : if (!q.exec(statement)) {
114 0 : qCDebug(logDb) << "Couldn't set DB version to " << version;
115 0 : qCDebug(logDb) << "Error: " << q.lastError().text();
116 : }
117 20 : }
118 :
119 10 : void DB::upgrade()
120 : {
121 10 : int initialVersion = getSchemaVersion();
122 10 : int nextVersion = initialVersion + 1;
123 : while(true) {
124 30 : QString filename;
125 30 : QTextStream output(&filename);
126 30 : output << ":/sql/sql/" << nextVersion << ".sql";
127 30 : QFile schemaFile(filename);
128 30 : if (!schemaFile.exists()) {
129 10 : break; // We're up to date!
130 : }
131 :
132 : // Do the upgrade.
133 20 : if (!executeSqlFile(schemaFile)) {
134 0 : return; // BAIL!
135 : }
136 :
137 20 : setSchemaVersion(nextVersion);
138 20 : nextVersion++;
139 70 : }
140 : }
141 :
142 20 : bool DB::executeSqlFile(QFile& sqlFile)
143 : {
144 20 : if (!sqlFile.open(QIODevice::ReadOnly)) {
145 0 : qCDebug(logDb) << "Could not open file: " << sqlFile.fileName();
146 0 : return false;
147 : }
148 :
149 : // Read file.
150 20 : QTextStream input(&sqlFile);
151 20 : QString entireFile = "";
152 20 : QString line;
153 350 : while(!input.atEnd()) {
154 330 : line = input.readLine().trimmed();
155 :
156 : // Ignore comments and whitespace.
157 330 : if (line.startsWith("--") || line.isEmpty()) {
158 152 : continue;
159 : }
160 :
161 178 : entireFile += line + "\n ";
162 : }
163 :
164 20 : sqlFile.close();
165 :
166 : // Split the file into individual SQL statements.
167 20 : QStringList statements;
168 20 : QString current;
169 20 : int depth = 0;
170 :
171 20 : const QStringList lines = entireFile.split("\n");
172 218 : for (const QString& rawLine : lines) {
173 198 : QString line = rawLine.trimmed();
174 198 : if (line.isEmpty())
175 20 : continue;
176 :
177 : // Check if this line starts a BEGIN block (e.g., CREATE TRIGGER ... BEGIN).
178 : // We look for BEGIN at the end of the line (after stripping whitespace).
179 178 : if (line.endsWith("BEGIN", Qt::CaseInsensitive)) {
180 6 : depth++;
181 : }
182 :
183 178 : current += rawLine + "\n";
184 :
185 178 : if (depth > 0) {
186 : // Inside a BEGIN...END block. Look for END; to close it.
187 78 : if (line.startsWith("END;", Qt::CaseInsensitive) ||
188 50 : line.startsWith("END ;", Qt::CaseInsensitive)) {
189 6 : depth--;
190 6 : if (depth == 0) {
191 : // Remove trailing semicolon from END; since we add the
192 : // whole block as one statement.
193 6 : statements << current.trimmed().chopped(1);
194 6 : current.clear();
195 : }
196 : }
197 : } else {
198 : // Outside a trigger body: split on semicolons normally.
199 150 : if (line.endsWith(";")) {
200 60 : statements << current.trimmed().chopped(1);
201 60 : current.clear();
202 : }
203 : }
204 198 : }
205 :
206 : // Handle any remaining text (statement without trailing semicolon).
207 20 : if (!current.trimmed().isEmpty()) {
208 0 : statements << current.trimmed();
209 : }
210 :
211 : // Execute each statement.
212 86 : for (const QString& s : std::as_const(statements)) {
213 66 : QString trimmed = s.trimmed();
214 66 : if (trimmed.isEmpty())
215 0 : continue;
216 :
217 66 : QSqlQuery q(db());
218 66 : if (!q.exec(trimmed)) {
219 0 : qCDebug(logDb) << "Could not execute sql statement: " << q.lastError().text();
220 0 : qCDebug(logDb) << trimmed;
221 0 : return false;
222 : }
223 66 : }
224 :
225 : // Great success!
226 20 : return true;
227 20 : }
|