LCOV - code coverage report
Current view: top level - src/db - DB.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 41.3 % 121 50
Test Date: 2026-03-23 10:19:47 Functions: 77.8 % 9 7

            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 : }
        

Generated by: LCOV version 2.0-1