From fd6bbbae8c5e3afcf6229bdcfd0c8160e3e59730 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:33:38 +0200 Subject: [PATCH 01/15] docs: update stale references in AGENTS.md and CONTEXT.md --- AGENTS.md | 2 +- CONTEXT.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ced9865..628748f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ implementation details. If you introduce a new domain term, add it to run SQL, it goes through the seam. The seam owns query construction. - **No widget pointers in non-GUI modules.** A module that takes a `QWidget*` is a presenter, not a domain module. Presenters are thin. -- **No SQL string concatenation in non-database modules.** `dbquery.h` +- **No SQL string concatenation in non-database modules.** `QueryExecutor` splitting-by-`;` lives in the database module, not in `mainwindow.cpp` or the CLI. diff --git a/CONTEXT.md b/CONTEXT.md index 97782b2..3edb915 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -22,7 +22,7 @@ architecture, the modules, and the tests all refer to the same things. ## User-facing actions -- **Query** — a single SQL statement the user wrote and wants executed. +- **Query** — a single SQL statement the user wrote and wants to execute. - **Script** — a sequence of Queries loaded from a `.sql` file. - **Export** — a one-shot transformation of Database content into a different representation. Two formats: **CSV** (one file per table) and **SQL** @@ -52,8 +52,9 @@ architecture, the modules, and the tests all refer to the same things. - **IDatabase** — abstract Database. Production adapter is `SqliteDatabase`; test adapter is `InMemoryDatabase`. Anything that takes a Database takes `IDatabase*`. `QSqlDatabase` does not leak across this seam. -- **ExportStrategy** — abstract DataExporter workflow (`SqlExportStrategy`, - `CsvExportStrategy`). Two adapters = real seam. +- **ExportOrchestrator** — runs a DataExporter in the background, marshals + progress to the GUI thread, surfaces cancel and completion via exportToSql() + and exportToCsv(). ## Out of scope (do not introduce) From ab980edba05cc865609376dcfd390c5356be0877 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:33:45 +0200 Subject: [PATCH 02/15] build: fail on missing tests, add Linux/macOS test execution, fix PATH --- build.ps1 | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/build.ps1 b/build.ps1 index 6506047..b074190 100644 --- a/build.ps1 +++ b/build.ps1 @@ -83,16 +83,19 @@ if ($IsWindows) { if (Test-Path $testExe) { $env:PATH = "$QtPath\bin;$QtPath\lib;$env:PATH" Write-Host "Executing: $testExe" - & $testExe - $env:PATH = $env:PATH -replace [regex]::Escape("$QtPath\bin;$QtPath\lib;"), "" - if ($LASTEXITCODE -ne 0) { - Write-Error "Tests failed (exit code: $LASTEXITCODE)" + Push-Location build + & .\SQLiteQueryTests.exe + $testExitCode = $LASTEXITCODE + Pop-Location + if ($testExitCode -ne 0) { + Write-Error "Tests failed (exit code: $testExitCode)" exit 1 } Write-Host "`n=== All tests passed ===" -ForegroundColor Green } else { - Write-Warning "Test executable not found at: $testExe" + Write-Error "Test executable not found at: $testExe" + exit 1 } } @@ -106,6 +109,30 @@ if ($IsLinux) { cmake --build build --config Release --parallel $(nproc) cmake --install build + Write-Host "`nRunning tests..." + $testExe = "./build/SQLiteQueryTests" + if (Test-Path $testExe) { + $originalPath = $env:PATH + if ($QtPath) { + $pathArray = ($env:PATH -split ':') | Where-Object { $_ -ne "$QtPath/bin" -and $_ -ne "$QtPath/lib" } + $pathArray = @("$QtPath/bin", "$QtPath/lib") + $pathArray + $env:PATH = $pathArray -join ':' + } + Write-Host "Executing: $testExe" + & $testExe + if ($QtPath) { + $env:PATH = $originalPath + } + if ($LASTEXITCODE -ne 0) { + Write-Error "Tests failed (exit code: $LASTEXITCODE)" + exit 1 + } + Write-Host "`n=== All tests passed ===" -ForegroundColor Green + } + else { + Write-Warning "Test executable not found at: $testExe" + } + if ($Package) { cpack -G 7Z --config ./build/CPackConfig.cmake cpack -G ZIP --config ./build/CPackConfig.cmake @@ -125,10 +152,35 @@ if ($IsLinux) { } } -if ($IsMacOS -And $Package) { +if ($IsMacOS) { cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/tmp/sqlitequery cmake --build build --config Release --parallel 32 + + Write-Host "`nRunning tests..." + $testExe = "./build/SQLiteQueryTests" + if (Test-Path $testExe) { + $originalPath = $env:PATH + if ($QtPath) { + $pathArray = ($env:PATH -split ':') | Where-Object { $_ -ne "$QtPath/bin" -and $_ -ne "$QtPath/lib" } + $pathArray = @("$QtPath/bin", "$QtPath/lib") + $pathArray + $env:PATH = $pathArray -join ':' + } + Write-Host "Executing: $testExe" + & $testExe + if ($QtPath) { + $env:PATH = $originalPath + } + if ($LASTEXITCODE -ne 0) { + Write-Error "Tests failed (exit code: $LASTEXITCODE)" + exit 1 + } + Write-Host "`n=== All tests passed ===" -ForegroundColor Green + } + else { + Write-Warning "Test executable not found at: $testExe" + } + if ($Package) { macdeployqt build/SQLiteQueryAnalyzer.app -dmg -appstore-compliant } From 72745ffc15b3b549ea3251a5d54dd32e7542149d Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:33:52 +0200 Subject: [PATCH 03/15] database: remove duplicate include, setHostName, add VACUUM error check --- src/database/dbanalyzer.cpp | 11 ++++------- src/database/sqlitedatabase.cpp | 12 ++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/database/dbanalyzer.cpp b/src/database/dbanalyzer.cpp index 4488af0..32631b3 100644 --- a/src/database/dbanalyzer.cpp +++ b/src/database/dbanalyzer.cpp @@ -2,10 +2,6 @@ #include -#include "dbanalyzer.h" - -#include - DbAnalyzer::DbAnalyzer(IDatabase *database) : database(database) { } @@ -26,13 +22,14 @@ bool DbAnalyzer::analyze(DatabaseInfo &info) const { void DbAnalyzer::loadTables(DatabaseInfo &info) const { const QString sql = "SELECT * FROM sqlite_master WHERE type='table'"; - if (!this->database->open()) { + const bool wasOpen = this->database->isOpen(); + if (!wasOpen && !this->database->open()) { return; } const QueryResult result = this->database->runStatement(sql); if (!result.ok) { - database->close(); + if (!wasOpen) database->close(); return; } @@ -47,7 +44,7 @@ void DbAnalyzer::loadTables(DatabaseInfo &info) const { info.tables.append(table); } - database->close(); + if (!wasOpen) database->close(); } void DbAnalyzer::loadColumns(DatabaseInfo &info) const { diff --git a/src/database/sqlitedatabase.cpp b/src/database/sqlitedatabase.cpp index 679c020..d0dd2ad 100644 --- a/src/database/sqlitedatabase.cpp +++ b/src/database/sqlitedatabase.cpp @@ -5,8 +5,9 @@ #include SqliteDatabase::SqliteDatabase() { - database = QSqlDatabase::addDatabase("QSQLITE"); - database.setHostName("localhost"); + static int instanceCounter = 0; + const QString connectionName = QString("sqlite_connection_%1").arg(++instanceCounter); + database = QSqlDatabase::addDatabase("QSQLITE", connectionName); } void SqliteDatabase::setSource(const QString &filename) { @@ -16,7 +17,8 @@ void SqliteDatabase::setSource(const QString &filename) { } bool SqliteDatabase::open() { - this->close(); + if (database.isOpen()) + return true; return database.open(); } @@ -30,7 +32,9 @@ void SqliteDatabase::shrink() { database.open(); QSqlQuery query(database); - query.exec("VACUUM"); + if (!query.exec("VACUUM")) { + qWarning("VACUUM failed: %s", qPrintable(query.lastError().text())); + } } QueryResult SqliteDatabase::runStatement(const QString &sql) { From 2682bbd546e0e0dcee5f9d0dc50348b11b8ea301 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:33:59 +0200 Subject: [PATCH 04/15] database: use unique connection names per SqliteDatabase instance --- src/database/sqlitedatabase.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/database/sqlitedatabase.h b/src/database/sqlitedatabase.h index ae366ee..c724704 100644 --- a/src/database/sqlitedatabase.h +++ b/src/database/sqlitedatabase.h @@ -16,6 +16,8 @@ class SqliteDatabase : public IDatabase { void close() override; + [[nodiscard]] bool isOpen() const override { return database.isOpen(); } + void shrink() override; [[nodiscard]] QString getFilename() const override { return source; } From 115ae8f77b079dd32c19346f3397ab9ffc86d6fd Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:12 +0200 Subject: [PATCH 05/15] database: add isOpen() to IDatabase interface and adapters --- src/database/idatabase.h | 2 ++ src/database/inmemorydatabase.h | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/database/idatabase.h b/src/database/idatabase.h index 19b45cb..21cec9e 100644 --- a/src/database/idatabase.h +++ b/src/database/idatabase.h @@ -19,6 +19,8 @@ class IDatabase { virtual void close() = 0; + [[nodiscard]] virtual bool isOpen() const = 0; + virtual void shrink() = 0; [[nodiscard]] virtual QString getFilename() const = 0; diff --git a/src/database/inmemorydatabase.h b/src/database/inmemorydatabase.h index 011d230..29d2799 100644 --- a/src/database/inmemorydatabase.h +++ b/src/database/inmemorydatabase.h @@ -16,6 +16,8 @@ class InMemoryDatabase : public IDatabase { void close() override; + [[nodiscard]] bool isOpen() const override { return database.isOpen(); } + void shrink() override; [[nodiscard]] QString getFilename() const override { return source; } @@ -25,8 +27,6 @@ class InMemoryDatabase : public IDatabase { QueryResult streamRows(const QString &sql, const std::function &)> &onRow) override; - [[nodiscard]] QSqlDatabase getConnection() const { return database; } - private: QSqlDatabase database; QString source; From a60e9d7dab977307974b0a02e0c9b56b32a05c61 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:19 +0200 Subject: [PATCH 06/15] database: reject setSource on in-memory DB, remove getConnection leak --- src/database/inmemorydatabase.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/database/inmemorydatabase.cpp b/src/database/inmemorydatabase.cpp index 08c903e..3f79426 100644 --- a/src/database/inmemorydatabase.cpp +++ b/src/database/inmemorydatabase.cpp @@ -7,10 +7,13 @@ InMemoryDatabase::InMemoryDatabase() { database = QSqlDatabase::addDatabase("QSQLITE", "in_memory_connection"); database.setDatabaseName(":memory:"); - database.setHostName("localhost"); } void InMemoryDatabase::setSource(const QString &filename) { + if (database.databaseName() == ":memory:") { + qWarning("InMemoryDatabase::setSource: cannot change source of in-memory database"); + return; + } this->source = filename; database.setDatabaseName(filename); } From 1486af1baf185a3779b0e4c0f71aeb727ca69e84 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:26 +0200 Subject: [PATCH 07/15] database: remove redundant Qt::CaseInsensitive, change rowsAffected to qint64 --- src/database/queryexecutor.cpp | 2 +- src/database/queryresult.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/queryexecutor.cpp b/src/database/queryexecutor.cpp index 1d3adeb..6f6ac47 100644 --- a/src/database/queryexecutor.cpp +++ b/src/database/queryexecutor.cpp @@ -12,7 +12,7 @@ QList QueryExecutor::runScript(const QString &script, QStringList * QList QueryExecutor::runStatements(const QStringList &statements, QStringList *errors) { QList results; for (const auto &raw: statements) { - const QString sql = raw.trimmed().replace('\n', "", Qt::CaseInsensitive); + const QString sql = raw.trimmed().replace('\n', ""); if (sql.isEmpty()) continue; diff --git a/src/database/queryresult.h b/src/database/queryresult.h index a32c492..47b4b11 100644 --- a/src/database/queryresult.h +++ b/src/database/queryresult.h @@ -16,7 +16,7 @@ struct QueryResult { bool isSelect = false; QStringList columns; QList rows; - int rowsAffected = 0; + qint64 rowsAffected = 0; }; #endif // QUERYRESULT_H From 540c40e1fb55631bb49004c88de0e7c9f5d8881c Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:34 +0200 Subject: [PATCH 08/15] database: add error tracking to ExportDataProgress, report streamRows failures --- src/database/dbexportdata.cpp | 11 +++++++++-- src/database/progress.h | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/database/dbexportdata.cpp b/src/database/dbexportdata.cpp index a421fef..d8f9681 100644 --- a/src/database/dbexportdata.cpp +++ b/src/database/dbexportdata.cpp @@ -64,12 +64,15 @@ void DbDataExport::exportDataToSqlFile(IDatabase *database, }); if (!streamResult.ok) { + progress->addError(table.name, streamResult.error); continue; } out << "\n"; } file->close(); - progress->setCompleted(); + if (!progress->hasAnyErrors()) { + progress->setCompleted(); + } } void DbDataExport::exportDataToCsvFile(IDatabase *database, @@ -106,9 +109,13 @@ void DbDataExport::exportDataToCsvFile(IDatabase *database, file->close(); if (!streamResult.ok) { + progress->addError(table.name, streamResult.error); + QFile::remove(filename); continue; } } - progress->setCompleted(); + if (!progress->hasAnyErrors()) { + progress->setCompleted(); + } } diff --git a/src/database/progress.h b/src/database/progress.h index 144cfa4..92d679b 100644 --- a/src/database/progress.h +++ b/src/database/progress.h @@ -1,15 +1,23 @@ #ifndef PROGRESS_H #define PROGRESS_H +#include +#include +#include + class ExportDataProgress { uint64_t affectedRows = 0; bool isComplete = false; bool printProgress = false; + bool hasErrors = false; + QStringList errors; public: void reset() { affectedRows = 0; isComplete = false; + hasErrors = false; + errors.clear(); } void increment() { @@ -22,8 +30,15 @@ class ExportDataProgress { } } + void addError(const QString &table, const QString &message) { + hasErrors = true; + errors.append(QString("[%1] %2").arg(table, message)); + } + [[nodiscard]] uint64_t getAffectedRows() const { return affectedRows; } [[nodiscard]] bool isCompleted() const { return isComplete; } + [[nodiscard]] bool hasAnyErrors() const { return hasErrors; } + [[nodiscard]] const QStringList &getErrors() const { return errors; } void setCompleted() { isComplete = true; } void setShowProgress(const bool value) { printProgress = value; } [[nodiscard]] bool isShowProgress() const { return printProgress; } From 49e8142865022e9b102ad5ea3ffb02cd9b578437 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:41 +0200 Subject: [PATCH 09/15] gui: fix ExportOrchestrator UAF and race condition with QPointer guards --- src/gui/exportorchestrator.cpp | 23 +++++++++++++++-------- src/gui/exportorchestrator.h | 2 ++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/gui/exportorchestrator.cpp b/src/gui/exportorchestrator.cpp index 32346c2..01a0134 100644 --- a/src/gui/exportorchestrator.cpp +++ b/src/gui/exportorchestrator.cpp @@ -36,21 +36,25 @@ void ExportOrchestrator::cancel() { } void ExportOrchestrator::onExportComplete() { + tearingDown_ = true; uint64_t rows = progress_->getAffectedRows(); - progress_.release(); - tcs_.release(); completed_ = true; + progress_.reset(); + tcs_.reset(); emit exportCompleted(rows); } void ExportOrchestrator::startProgressPolling(CancellationToken token) { - auto _ = QtConcurrent::run([this, token]() { + QPointer guard(this); + auto _ = QtConcurrent::run([this, token, guard]() { do { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (completed_) + if (completed_ || tearingDown_) break; - MainThread::run([this]() { - emit exportProgress(progress_->getAffectedRows()); + MainThread::run([guard]() { + if (!guard || guard->tearingDown_) + return; + emit guard->exportProgress(guard->progress_->getAffectedRows()); }); } while (!token.isCancellationRequested() && !progress_->isCompleted()); @@ -70,8 +74,11 @@ void ExportOrchestrator::runExport(IDatabase *db, DatabaseInfo info, }); future.then([this]() { - MainThread::run([this]() { - this->onExportComplete(); + QPointer guard(this); + MainThread::run([guard]() { + if (!guard || guard->tearingDown_) + return; + guard->onExportComplete(); }); }); diff --git a/src/gui/exportorchestrator.h b/src/gui/exportorchestrator.h index ad4441b..4496b1e 100644 --- a/src/gui/exportorchestrator.h +++ b/src/gui/exportorchestrator.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "../threading/cancellation.h" #include "../threading/mainthread.h" @@ -42,6 +43,7 @@ private slots: std::unique_ptr progress_; std::unique_ptr tcs_; std::atomic completed_{false}; + std::atomic tearingDown_{false}; }; #endif // EXPORTORCHESTRATOR_H From 0510189ad4fa54d21f52f4e32f15ee7ed4abafeb Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:48 +0200 Subject: [PATCH 10/15] gui: decouple MainWindow from SqliteDatabase, use IDatabase interface --- src/gui/mainwindow.cpp | 28 +++++++++++++--------------- src/gui/mainwindow.h | 4 ++-- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 3e1a131..d10141e 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "ui_mainwindow.h" +#include "../database/sqlitedatabase.h" #include "../settings/settings.h" #include "../database/dbexportschema.h" #include "prompts.h" @@ -339,13 +340,13 @@ void MainWindow::executeQuery() const { return; } - QStringList list(ui->textEdit->toPlainText().split(";", Qt::SkipEmptyParts)); + const QString script = ui->textEdit->toPlainText(); QStringList errors; QElapsedTimer time; time.start(); - if (this->queryPresenter->execute(list, &errors)) { + if (this->queryPresenter->executeScript(script, &errors)) { ui->tabWidget->setCurrentIndex(0); ui->queryResultTab->setCurrentIndex(0); } @@ -354,15 +355,12 @@ void MainWindow::executeQuery() const { const auto msg = "Query execution took " + QString::number(milliseconds / 1000) + " seconds"; this->showMessage(msg); - foreach(const QString sql, list) { - if (sql.contains("create", Qt::CaseInsensitive) || - sql.contains("drop", Qt::CaseInsensitive) || - sql.contains("insert", Qt::CaseInsensitive) || - sql.contains("delete", Qt::CaseInsensitive)) { - ui->queryResultTab->setCurrentIndex(1); - analyzeDatabase(); - break; - } + if (script.contains("create", Qt::CaseInsensitive) || + script.contains("drop", Qt::CaseInsensitive) || + script.contains("insert", Qt::CaseInsensitive) || + script.contains("delete", Qt::CaseInsensitive)) { + ui->queryResultTab->setCurrentIndex(1); + analyzeDatabase(); } } @@ -398,9 +396,9 @@ void MainWindow::exportDataToSqlScript() { exportOrchestrator->exportToSql(std::move(info), filepath, database.get()); connect(exportOrchestrator.get(), &ExportOrchestrator::exportProgress, - this, &MainWindow::onExportProgress); + this, &MainWindow::onExportProgress, Qt::UniqueConnection); connect(exportOrchestrator.get(), &ExportOrchestrator::exportCompleted, - this, &MainWindow::onExportCompleted); + this, &MainWindow::onExportCompleted, Qt::UniqueConnection); } void MainWindow::exportDataToCsvFiles() { @@ -420,9 +418,9 @@ void MainWindow::exportDataToCsvFiles() { exportOrchestrator->exportToCsv(std::move(info), outputFolder, delimiter, database.get()); connect(exportOrchestrator.get(), &ExportOrchestrator::exportProgress, - this, &MainWindow::onExportProgress); + this, &MainWindow::onExportProgress, Qt::UniqueConnection); connect(exportOrchestrator.get(), &ExportOrchestrator::exportCompleted, - this, &MainWindow::onExportCompleted); + this, &MainWindow::onExportCompleted, Qt::UniqueConnection); } void MainWindow::cancel() const { diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 516a2c7..5dbe28d 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -9,8 +9,8 @@ #include "../database/dbexport.h" #include "../database/dbexportdata.h" #include "../database/dbtree.h" +#include "../database/idatabase.h" #include "../database/queryexecutor.h" -#include "../database/sqlitedatabase.h" #include "highlighter.h" #include "queryexecutionpresenter.h" #include "sessionmanager.h" @@ -81,7 +81,7 @@ public slots: std::unique_ptr ui; std::unique_ptr recentFilesMenu; std::unique_ptr statusBar; - std::unique_ptr database; + std::unique_ptr database; std::unique_ptr analyzer; std::unique_ptr executor; std::unique_ptr queryPresenter; From 1adeb292aceb284901898a695184a7468a12227d Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:34:58 +0200 Subject: [PATCH 11/15] gui: fix QueryResultPresenter widget and model memory leaks --- src/gui/queryresultpresenter.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/gui/queryresultpresenter.cpp b/src/gui/queryresultpresenter.cpp index 62dff2b..0e2bd17 100644 --- a/src/gui/queryresultpresenter.cpp +++ b/src/gui/queryresultpresenter.cpp @@ -23,6 +23,9 @@ void QueryResultPresenter::clear() { } void QueryResultPresenter::present(const QList &results) { + qDeleteAll(this->tableViews.begin(), this->tableViews.end()); + this->tableViews.clear(); + const QRect widgetRect = this->widget->geometry(); const int width = widgetRect.width(); const int height = widgetRect.height(); @@ -40,7 +43,7 @@ void QueryResultPresenter::present(const QList &results) { auto model = std::make_unique( static_cast(result.rows.size()), static_cast(result.columns.size()), - container.get()); + nullptr); for (int col = 0; col < result.columns.size(); ++col) { model->setHorizontalHeaderItem(col, new QStandardItem(result.columns.at(col))); @@ -67,10 +70,14 @@ void QueryResultPresenter::present(const QList &results) { } void QueryResultPresenter::presentToView(QTableView *view, const QueryResult &result) { + if (view->model() != nullptr) { + view->setModel(nullptr); + } + auto model = std::make_unique( static_cast(result.rows.size()), static_cast(result.columns.size()), - view); + nullptr); for (int col = 0; col < result.columns.size(); ++col) { model->setHorizontalHeaderItem(col, new QStandardItem(result.columns.at(col))); @@ -83,6 +90,7 @@ void QueryResultPresenter::presentToView(QTableView *view, const QueryResult &re } } + model->setParent(view); view->setModel(model.release()); view->setSortingEnabled(true); } From ecc7232dbac374a012e1c77756c9908429a924c5 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:35:04 +0200 Subject: [PATCH 12/15] gui: add executeScript to QueryExecutionPresenter, use UniqueConnection --- src/gui/queryexecutionpresenter.cpp | 6 ++++++ src/gui/queryexecutionpresenter.h | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/gui/queryexecutionpresenter.cpp b/src/gui/queryexecutionpresenter.cpp index 892649d..d64c2e3 100644 --- a/src/gui/queryexecutionpresenter.cpp +++ b/src/gui/queryexecutionpresenter.cpp @@ -11,6 +11,12 @@ bool QueryExecutionPresenter::execute(const QStringList &statements, QStringList return errors == nullptr || errors->isEmpty(); } +bool QueryExecutionPresenter::executeScript(const QString &script, QStringList *errors) { + const QList results = executor->runScript(script, errors); + presenter->present(results); + return errors == nullptr || errors->isEmpty(); +} + void QueryExecutionPresenter::clearResults() { presenter->clear(); } diff --git a/src/gui/queryexecutionpresenter.h b/src/gui/queryexecutionpresenter.h index bf105b7..3a1d02f 100644 --- a/src/gui/queryexecutionpresenter.h +++ b/src/gui/queryexecutionpresenter.h @@ -16,6 +16,8 @@ class QueryExecutionPresenter { bool execute(const QStringList &statements, QStringList *errors = nullptr); + bool executeScript(const QString &script, QStringList *errors = nullptr); + void clearResults(); void presentToView(QTableView *view, const QueryResult &result); From 97ef5e456d5761910d4305ce41d62a8d03637b0f Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:35:19 +0200 Subject: [PATCH 13/15] cli: output execution errors and exit non-zero on failure --- src/cli/script.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cli/script.cpp b/src/cli/script.cpp index ff8472b..106f89e 100644 --- a/src/cli/script.cpp +++ b/src/cli/script.cpp @@ -29,9 +29,20 @@ void Script::executeSqlFile(const QString &sqlFilePath, QStringList errors; executor.runScript(sqlScript, &errors); + if (!errors.isEmpty()) { + QTextStream err(stderr); + for (const auto &error : errors) { + err << "ERROR: " << error << "\n"; + } + } + const auto milliseconds = static_cast(time.elapsed()); const auto msg = "Script execution took " + QString::number(milliseconds / 1000) + " seconds"; QTextStream out(stdout); out << msg; fflush(stdout); + + if (!errors.isEmpty()) { + exit(1); + } } From cdd9fd7d3ba4ad37276cfcfa1f7edf966e6e33ad Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:35:26 +0200 Subject: [PATCH 14/15] tests: support Qt5/Qt6, remove progress.h from compiled sources --- tests/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 391a0c1..36b52b3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,7 +4,8 @@ project(SQLiteQueryTests LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Sql) +find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core Sql) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Sql) # Fetch Google Test include(FetchContent) @@ -36,7 +37,6 @@ set(PROD_SRCS ../src/database/dbexportdata.cpp ../src/database/dbexportschema.cpp ../src/database/dbexport.cpp - ../src/database/progress.h ../src/threading/cancellation.cpp ../src/gui/exportorchestrator.cpp ) @@ -44,9 +44,9 @@ set(PROD_SRCS target_sources(${PROJECT_NAME} PRIVATE ${PROD_SRCS}) target_link_libraries(${PROJECT_NAME} PRIVATE - Qt6::Core - Qt6::Gui - Qt6::Sql + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Sql GTest::gtest_main GTest::gtest ) From 6c2e88e6718b7535aae4aedf46f1758c6d925b10 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Wed, 10 Jun 2026 00:35:32 +0200 Subject: [PATCH 15/15] tests: document FK/UNIQUE limitation in schema export test --- tests/test_dbexportschema.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_dbexportschema.cpp b/tests/test_dbexportschema.cpp index aebc47c..222ef51 100644 --- a/tests/test_dbexportschema.cpp +++ b/tests/test_dbexportschema.cpp @@ -75,7 +75,11 @@ TEST_F(DbSchemaExportTest, ExportSchemaContainsConstraints) { // PRIMARY KEY and NOT NULL are preserved during analysis EXPECT_TRUE(schema.contains("PRIMARY KEY")); EXPECT_TRUE(schema.contains("NOT NULL")); - // FOREIGN KEY and UNIQUE are lost during analysis (Column struct has no FK/unique fields) + // TODO: FOREIGN KEY and UNIQUE are lost during analysis — Column struct + // has no FK/unique fields. DbAnalyzer.analyze() only captures name, type, + // notNull, defaultValue, and primaryKey. DbSchemaExport/exportSchema + // reflects this limitation. Consider extending Column to store FK/UNIQUE + // in a future iteration. } TEST_F(DbSchemaExportTest, ExportSchemaToFile) {