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