Skip to content

Enhance database handling, GUI stability, and testing support - #112

Closed
christianhelle wants to merge 15 commits into
masterfrom
improve-codebase-architecture
Closed

Enhance database handling, GUI stability, and testing support#112
christianhelle wants to merge 15 commits into
masterfrom
improve-codebase-architecture

Conversation

@christianhelle

@christianhelle christianhelle commented Jun 9, 2026

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and bug fixes across the build scripts, database modules, error handling, and export orchestration logic. The main changes enhance cross-platform test execution, improve error reporting during exports and script execution, and ensure safer resource management in asynchronous GUI operations. Several interface and implementation details have also been clarified or refactored for maintainability.

Build and Test Improvements

  • Improved cross-platform test execution in build.ps1 for Windows, Linux, and macOS, ensuring that tests are always run after building and that errors are handled consistently. Now, missing test executables are treated as errors, and environment variables for Qt are managed more robustly. [1] [2] [3]

Database and Export Error Handling

  • Enhanced error reporting in data export and script execution:
    • ExportDataProgress now tracks errors per table, and errors are surfaced to the user when they occur during export. [1] [2] [3] [4]
    • The CLI script runner now prints all SQL errors and exits with a non-zero code if any errors occur.
  • The IDatabase interface now includes an isOpen() method, and both SqliteDatabase and InMemoryDatabase implement it, allowing for safer connection management. [1] [2] [3]

Resource and Concurrency Safety in GUI Export

  • The ExportOrchestrator now uses QPointer and a tearingDown_ flag to prevent race conditions and use-after-free bugs during asynchronous export operations and progress polling. [1] [2] [3] [4]

Refactoring and Maintenance

  • Fixed connection management in SqliteDatabase by assigning unique connection names and only opening the database if not already open. [1] [2]
  • Improved logic in DbAnalyzer to avoid unnecessary open/close cycles and only close the database if it was not already open. [1] [2]
  • Removed redundant code and clarified error handling in several database modules. [1] [2] [3] [4] [5]

Documentation and Context Updates

  • Updated documentation to clarify terminology and module responsibilities, such as replacing ExportStrategy with ExportOrchestrator and clarifying the definition of a Query. [1] [2] [3]

These changes collectively improve reliability, maintainability, and user feedback in both the CLI and GUI applications.

Summary by CodeRabbit

  • New Features

    • Export operations now track and report errors during data export, with automatic cleanup of incomplete files.
    • Query execution now provides detailed error reporting with proper exit codes.
  • Bug Fixes

    • Export completion only occurs when no errors are encountered, preventing false success states.
    • Improved database connection handling and lifecycle management.
  • Chores

    • Enhanced Qt framework compatibility (Qt5 and Qt6 support).
    • Documentation updates to domain vocabulary and architectural guidelines.

@christianhelle christianhelle self-assigned this Jun 9, 2026
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors database connection lifecycle management, hardens export operations with per-table error tracking, transforms query execution to script-based processing, adds teardown safety to the export orchestrator with pointer guards, and updates build/tooling for Qt5/Qt6 compatibility and safer memory management in result presentation.

Changes

Database Lifecycle, Export Robustness, and Concurrency Safety

Layer / File(s) Summary
Database interface and lifecycle contract
src/database/idatabase.h, src/database/sqlitedatabase.h, src/database/sqlitedatabase.cpp, src/database/inmemorydatabase.h, src/database/inmemorydatabase.cpp, src/database/dbanalyzer.cpp
IDatabase interface adds isOpen() method. SqliteDatabase uses unique per-instance connection names instead of default/global connection, checks if already open before reopening, and validates VACUUM errors. InMemoryDatabase prevents source changes on existing :memory: connections and implements isOpen(). DbAnalyzer tracks prior-open state to conditionally close databases only when opened by the function.
Export error tracking and conditional completion
src/database/progress.h, src/database/dbexportdata.cpp
ExportDataProgress introduces error tracking with hasErrors flag, errors list, and methods addError(), hasAnyErrors(), getErrors(). Export functions record per-table streaming failures, delete partially-written CSV files on failure, and defer setCompleted() until after loop only when no errors exist.
Script-based query execution and presenter
src/database/queryexecutor.cpp, src/gui/queryexecutionpresenter.h, src/gui/queryexecutionpresenter.cpp, src/gui/mainwindow.cpp
QueryExecutor removes case-insensitive flag from SQL normalization. QueryExecutionPresenter gains executeScript() method to execute single scripts. MainWindow::executeQuery switches from splitting on semicolons to executing full script via presenter, with script-wide keyword detection for schema re-analysis.
MainWindow abstraction to IDatabase interface
src/gui/mainwindow.h, src/gui/mainwindow.cpp
MainWindow decouples from SqliteDatabase by switching database member to std::unique_ptr<IDatabase> and updating includes to use abstract interface.
ExportOrchestrator teardown safety and QPointer guards
src/gui/exportorchestrator.h, src/gui/exportorchestrator.cpp
ExportOrchestrator adds atomic tearingDown_ flag. onExportComplete() sets teardown before finalizing completion. startProgressPolling() guards background thread with QPointer and stops loop on teardown. future.then() callback uses QPointer guard before invoking completion.
Result presenter view cleanup and model parenting
src/gui/queryresultpresenter.cpp
QueryResultPresenter::present() deletes and clears old table views before building new ones. QStandardItemModel uses nullptr parent; presentToView() explicitly sets parent via setParent(view) and detaches prior models with setModel(nullptr).
CLI error reporting, build test execution, Qt version support
src/cli/script.cpp, build.ps1, tests/CMakeLists.txt, src/database/queryresult.h
CLI script execution prints errors to stderr and exits with code 1 on failure. build.ps1 consolidates Windows/Linux/macOS test runners with Qt PATH management. CMake detects Qt5 or Qt6 and links against Qt${QT_VERSION_MAJOR} targets. QueryResult::rowsAffected changes to qint64.
Documentation and comment updates
AGENTS.md, CONTEXT.md, tests/test_dbexportschema.cpp
AGENTS.md references QueryExecutor instead of dbquery.h. CONTEXT.md updates Query vocabulary and ExportOrchestrator module description. Test comments clarify schema constraint limitations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • christianhelle/sqlitequery#110: Introduces QueryExecutor refactoring and foundational ExportOrchestrator architecture that this PR builds upon with added error tracking and teardown safety.

Suggested labels

enhancement

Poem

🐰 A rabbit hops through database dreams so bright,
Scripts now flow whole—no more semicolon splits tonight!
Errors are caught with care, from export's gentle hand,
QPointers guard the teardown dance across the land.
Qt5, Qt6, memory safe and sound—
Quality bounds with every safety bound!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Enhance database handling, GUI stability, and testing support' accurately summarizes the main themes across the multi-faceted PR: database improvements (connection management, error handling), GUI enhancements (concurrency/safety), and testing support (cross-platform build/test execution).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improve-codebase-architecture

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
src/cli/script.cpp

In file included from src/cli/script.cpp:1:
src/cli/script.h:4:10: fatal error: 'QString' file not found
4 | #include
| ^~~~~~~~~
1 error generated.
src/cli/script.cpp:11:56-48:1: ERROR translating statement 'CompoundStmt'
Aborting translation of method 'Script::executeSqlFile' in file 'src/cli/script.cpp': "Assert_failure src/clang/cAst_utils.ml:249:53"
Uncaught Internal Error: "Assert_failure src/clang/cAst_utils.ml:249:53"
Error backtrace:
Raised at ClangFrontend__CAst_utils.get_decl_from_typ_ptr in file "src/clang/cAst_utils.ml", line 249, characters 53-65
Called from ClangFrontend__CTrans.CTrans_funct.get_destructor_decl_ref in file "src/clang/cTrans.ml", line 658, characters 12-59
Called from ClangFrontend__CTrans.CTrans_funct.destructor_calls.(fun) in file "src/clang/cTrans.ml", line 2048, characters 12-69
Called from Base__List.rev_filter_map.loop in file "src/list.ml", line 944, characters 13-17
Called from Base__List.filter_map in file "src/list.m

... [truncated 2200 characters] ...

e 54, characters 4-52
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.process_method_decl.add_method_if_create_procdesc in file "src/clang/cFrontend_decl.ml" (inlined), line 123, characters 16-158
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.process_method_decl in file "src/clang/cFrontend_decl.ml", line 126, characters 17-97
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.process_methods in file "src/clang/cFrontend_decl.ml" (inlined), line 270, characters 8-122
Called from Stdlib__List.iter in file "list.ml" (inlined), line 110, characters 12-15
Called from Stdlib__List.iter in file "list.ml" (inlined), line 108, characters 13-64
Called from Base__List0.iter in file "src/list0.ml" (inlined), line 25, characters 16-35
Called from ClangFronte

src/database/dbanalyzer.cpp

In file included from src/database/dbanalyzer.cpp:1:
In file included from src/database/dbanalyzer.h:4:
src/database/databaseinfo.h:4:10: fatal error: 'QtGlobal' file not found
4 | #include
| ^~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/6c2e88e6718b7535aae4aedf46f1758c6d925b10-115a212d04d8ce5c/tmp/clang_command_.tmp.d2984f.txt
++Contents of '/tmp/coderabbit-infer/6c2e88e6718b7535aae4aedf46f1758c6d925b10-115a212d04d8ce5c/tmp/clang_command_.tmp.d2984f.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535

... [truncated 1158 characters] ...

install/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/115a212d04d8ce5c/file.o" "-x" "c++"
"src/database/dbanalyzer.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

src/database/inmemorydatabase.cpp

In file included from src/database/inmemorydatabase.cpp:1:
src/database/inmemorydatabase.h:4:10: fatal error: 'QSqlDatabase' file not found
4 | #include
| ^~~~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/6c2e88e6718b7535aae4aedf46f1758c6d925b10-dd7eb15aea2b3468/tmp/clang_command_.tmp.2e465f.txt
++Contents of '/tmp/coderabbit-infer/6c2e88e6718b7535aae4aedf46f1758c6d925b10-dd7eb15aea2b3468/tmp/clang_command_.tmp.2e465f.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_6

... [truncated 1141 characters] ...

l/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/dd7eb15aea2b3468/file.o" "-x" "c++"
"src/database/inmemorydatabase.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

  • 8 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
29.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/database/dbexportdata.cpp (1)

40-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Record file-open failures in ExportDataProgress.

Line 40 and Line 91 return before adding an error, but completion/reporting now depends on hasAnyErrors() (Line 73 and Line 118). This can surface failed exports as successful upstream.

Suggested fix
 void DbDataExport::exportDataToSqlFile(IDatabase *database,
                                        const QString &filename,
                                        const CancellationToken *cancellationToken,
                                        ExportDataProgress *progress) const {
+    progress->reset();
     const auto file = std::make_unique<QFile>(filename);
     if (!file->
         open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
+        progress->addError("export", QString("Failed to open SQL output: %1").arg(filename));
         return;
     }
-
-    progress->reset();
     QTextStream out(file.get());
@@
 void DbDataExport::exportDataToCsvFile(IDatabase *database,
                                        const QString &outputFolder,
                                        const QString &delimiter,
                                        const CancellationToken *cancellationToken,
                                        ExportDataProgress *progress) const {
     progress->reset();
@@
         const auto file = std::make_unique<QFile>(filename);
         if (!file->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
-            return;
+            progress->addError(table.name, QString("Failed to open CSV output: %1").arg(filename));
+            continue;
         }

Also applies to: 73-75, 91-93, 118-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/dbexportdata.cpp` around lines 40 - 43, The code returns early
when QFile::open(...) fails without recording the failure in ExportDataProgress,
so upstream hasAnyErrors() may miss it; update every early-return file-open
failure (the file->open(...) checks around the file write sections) to call
ExportDataProgress to record the error before returning — e.g. call
progress.addError() or ExportDataProgress::addError(...) with a descriptive
message that includes the file name (file->fileName()) and the
QFile::errorString() (or errno/text) so the failure is recorded for
hasAnyErrors() and reporting.
🧹 Nitpick comments (1)
src/database/dbanalyzer.cpp (1)

25-27: ⚡ Quick win

Avoid open/close churn across one analysis pass.

loadTables() now closes when it opened the DB, but loadColumns() immediately opens again in the same analyze() flow. Consider owning open/close once around both steps to avoid redundant connection transitions.

Suggested refactor
 bool DbAnalyzer::analyze(DatabaseInfo &info) const {
+    const bool wasOpen = this->database->isOpen();
+    if (!wasOpen && !this->database->open()) {
+        return false;
+    }
+
     const QFileInfo file(this->database->getFilename());
@@
     loadTables(info);
     loadColumns(info);
+
+    if (!wasOpen) {
+        this->database->close();
+    }
 
     return true;
 }

Also applies to: 32-33, 47-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/dbanalyzer.cpp` around lines 25 - 27, The analyze() flow
currently lets loadTables() open and close the DB then loadColumns() re-opens
it, causing redundant churn; modify analyze(), loadTables(), and loadColumns()
so analyze() owns the open/close lifecycle: in analyze() check
database->isOpen(), call database->open() if needed and remember whether you
opened it, then call loadTables() and loadColumns() without them performing
open/close; remove or guard the database->open()/close() calls inside
loadTables() and loadColumns() (or make them conditional on not already-open)
and ensure at the end of analyze() you only call database->close() if you were
the opener (using the saved wasOpen flag).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.ps1`:
- Around line 155-182: The macOS branch currently treats a missing test
executable with Write-Warning which is inconsistent with the Windows behavior;
update the macOS test-executable handling so that when Test-Path $testExe is
false it calls Write-Error with a descriptive message and then exits with exit 1
(same as Windows), ensuring $testExe, Test-Path, Write-Error and exit 1 are used
to signal a failure rather than just warning.
- Around line 112-134: The script treats a missing test executable differently
on Linux vs Windows; locate the block that checks Test-Path for $testExe (uses
Test-Path, Write-Warning, Write-Host "Executing: $testExe", and the $QtPath PATH
manipulation) and change the else branch so it mirrors the Windows behavior:
replace the Write-Warning with a Write-Error including the missing path and then
call exit 1 so missing test executables are treated as errors consistently
across platforms; preserve the surrounding PATH restore logic tied to $QtPath
and ensure $LASTEXITCODE handling remains unchanged after running the
executable.

In `@CONTEXT.md`:
- Around line 55-57: Remove ExportOrchestrator from the Seams section: edit the
Seams list to only include abstract interfaces (e.g., IDatabase) and delete the
ExportOrchestrator bullet/description (ExportOrchestrator is a concrete
orchestrator already documented under Modules). Ensure the Seams section
contains only true seams (abstract interfaces with multiple adapters) and leave
the Modules section entry for ExportOrchestrator unchanged.

In `@src/database/queryexecutor.cpp`:
- Line 15: The current assignment to QString sql uses
raw.trimmed().replace('\n', "") which removes newlines and can glue tokens;
change it to normalize whitespace instead: replace newlines and other whitespace
runs with a single space and trim the result so multiline SQL like "SELECT
...\nFROM ..." becomes "SELECT ... FROM ...". Update the expression that sets
sql (the line creating QString sql from raw) to perform a regex-based whitespace
collapse (e.g., replace sequences of whitespace characters with a single space)
and then call trimmed() so you preserve token separation while still removing
excess whitespace.

In `@src/gui/exportorchestrator.cpp`:
- Line 39: The field tearingDown_ is set to true during teardown but never
reset, causing subsequent calls to runExport to short-circuit; update the export
startup logic (e.g., at the top of runExport or wherever a new export job is
initialized) to explicitly set tearingDown_ = false before proceeding, and
ensure any early-return paths that begin a fresh export also reset tearingDown_;
check the teardown path where tearingDown_ is set true (the existing assignment)
remains but mirror it with the reset at export start so polling/completion logic
sees the correct state.

In `@src/gui/mainwindow.cpp`:
- Around line 396-401: Move the connect calls to attach ExportOrchestrator
signals before invoking export work so slots can't miss immediate
progress/completion; specifically, for calls like
exportOrchestrator->exportToSql(std::move(info), filepath, database.get()) and
exportOrchestrator->exportToCsv(...), call connect(exportOrchestrator.get(),
&ExportOrchestrator::exportProgress, this, &MainWindow::onExportProgress,
Qt::UniqueConnection) and connect(exportOrchestrator.get(),
&ExportOrchestrator::exportCompleted, this, &MainWindow::onExportCompleted,
Qt::UniqueConnection) first, then start the export; ensure you preserve the
std::move(info) semantics and apply the same reorder for the other block
(exportToCsv).
- Around line 358-364: The refresh trigger only looks for "create", "drop",
"insert", "delete" in the SQL string, so ALTER/RENAME statements don't call
analyzeDatabase(); update the conditional that examines the variable script (the
same block that calls ui->queryResultTab->setCurrentIndex(1) and
analyzeDatabase()) to also detect "alter" and "rename" (case-insensitive) so
schema-changing statements trigger ui refresh; keep the same behavior of
switching to the results tab before calling analyzeDatabase().

In `@src/gui/queryresultpresenter.cpp`:
- Around line 43-47: QueryResultPresenter::present() currently creates a
QStandardItemModel with a nullptr parent and then releases it to
tablePtr->setModel(...), causing leaked models; fix by giving the model a
QObject parent tied to the QTableView before releasing ownership (either
construct the QStandardItemModel with tablePtr as parent or call
model->setParent(tablePtr) prior to model.release()), mirroring presentToView()
behavior so models are deleted with their view.
- Around line 73-75: In QueryResultPresenter (around the view refresh code), the
existing code only calls view->setModel(nullptr) which detaches but does not
delete the previous model (the new models are parented to view later around the
create-model logic), causing accumulated QObject children; fix by explicitly
deleting the old model before detaching—e.g., if (auto old = view->model()) {
delete old; view->setModel(nullptr); }—so replace the current
view->setModel(nullptr) branch to delete view->model() first to prevent leaks.

---

Outside diff comments:
In `@src/database/dbexportdata.cpp`:
- Around line 40-43: The code returns early when QFile::open(...) fails without
recording the failure in ExportDataProgress, so upstream hasAnyErrors() may miss
it; update every early-return file-open failure (the file->open(...) checks
around the file write sections) to call ExportDataProgress to record the error
before returning — e.g. call progress.addError() or
ExportDataProgress::addError(...) with a descriptive message that includes the
file name (file->fileName()) and the QFile::errorString() (or errno/text) so the
failure is recorded for hasAnyErrors() and reporting.

---

Nitpick comments:
In `@src/database/dbanalyzer.cpp`:
- Around line 25-27: The analyze() flow currently lets loadTables() open and
close the DB then loadColumns() re-opens it, causing redundant churn; modify
analyze(), loadTables(), and loadColumns() so analyze() owns the open/close
lifecycle: in analyze() check database->isOpen(), call database->open() if
needed and remember whether you opened it, then call loadTables() and
loadColumns() without them performing open/close; remove or guard the
database->open()/close() calls inside loadTables() and loadColumns() (or make
them conditional on not already-open) and ensure at the end of analyze() you
only call database->close() if you were the opener (using the saved wasOpen
flag).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d2f6d55-a93c-43de-ae3c-60903ad252dd

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc69ec and 6c2e88e.

📒 Files selected for processing (23)
  • AGENTS.md
  • CONTEXT.md
  • build.ps1
  • src/cli/script.cpp
  • src/database/dbanalyzer.cpp
  • src/database/dbexportdata.cpp
  • src/database/idatabase.h
  • src/database/inmemorydatabase.cpp
  • src/database/inmemorydatabase.h
  • src/database/progress.h
  • src/database/queryexecutor.cpp
  • src/database/queryresult.h
  • src/database/sqlitedatabase.cpp
  • src/database/sqlitedatabase.h
  • src/gui/exportorchestrator.cpp
  • src/gui/exportorchestrator.h
  • src/gui/mainwindow.cpp
  • src/gui/mainwindow.h
  • src/gui/queryexecutionpresenter.cpp
  • src/gui/queryexecutionpresenter.h
  • src/gui/queryresultpresenter.cpp
  • tests/CMakeLists.txt
  • tests/test_dbexportschema.cpp

Comment thread build.ps1
Comment on lines +112 to +134
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"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inconsistent handling of missing test executables across platforms.

On Windows (lines 97-98), a missing test executable triggers Write-Error and exit 1. However, on Linux (line 133), it only emits Write-Warning and continues. This violates the stated PR objective that "missing test executables are treated as errors."

🔧 Proposed fix to make Linux consistent with Windows
     else {
-        Write-Warning "Test executable not found at: $testExe"
+        Write-Error "Test executable not found at: $testExe"
+        exit 1
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.ps1` around lines 112 - 134, The script treats a missing test
executable differently on Linux vs Windows; locate the block that checks
Test-Path for $testExe (uses Test-Path, Write-Warning, Write-Host "Executing:
$testExe", and the $QtPath PATH manipulation) and change the else branch so it
mirrors the Windows behavior: replace the Write-Warning with a Write-Error
including the missing path and then call exit 1 so missing test executables are
treated as errors consistently across platforms; preserve the surrounding PATH
restore logic tied to $QtPath and ensure $LASTEXITCODE handling remains
unchanged after running the executable.

Comment thread build.ps1
Comment on lines +155 to +182
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"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inconsistent handling of missing test executables across platforms.

On Windows (lines 97-98), a missing test executable triggers Write-Error and exit 1. However, on macOS (line 181), it only emits Write-Warning and continues. This violates the stated PR objective that "missing test executables are treated as errors."

🔧 Proposed fix to make macOS consistent with Windows
     else {
-        Write-Warning "Test executable not found at: $testExe"
+        Write-Error "Test executable not found at: $testExe"
+        exit 1
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.ps1` around lines 155 - 182, The macOS branch currently treats a
missing test executable with Write-Warning which is inconsistent with the
Windows behavior; update the macOS test-executable handling so that when
Test-Path $testExe is false it calls Write-Error with a descriptive message and
then exits with exit 1 (same as Windows), ensuring $testExe, Test-Path,
Write-Error and exit 1 are used to signal a failure rather than just warning.

Comment thread CONTEXT.md
Comment on lines +55 to +57
- **ExportOrchestrator** — runs a DataExporter in the background, marshals
progress to the GUI thread, surfaces cancel and completion via exportToSql()
and exportToCsv().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ExportOrchestrator is not a seam and should not be listed in the Seams section.

Per the coding guidelines, seams must be abstract interfaces with at least two adapters. ExportOrchestrator is a concrete orchestrator class, not an abstraction. This description already exists correctly in the Modules section (lines 44-45).

The Seams section should only list abstract interfaces like IDatabase. Remove ExportOrchestrator from here to avoid confusion about the architecture.

📐 Proposed fix
 ## Seams
 
 - **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.
-- **ExportOrchestrator** — runs a DataExporter in the background, marshals
-  progress to the GUI thread, surfaces cancel and completion via exportToSql()
-  and exportToCsv().
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **ExportOrchestrator** — runs a DataExporter in the background, marshals
progress to the GUI thread, surfaces cancel and completion via exportToSql()
and exportToCsv().
## Seams
- **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.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTEXT.md` around lines 55 - 57, Remove ExportOrchestrator from the Seams
section: edit the Seams list to only include abstract interfaces (e.g.,
IDatabase) and delete the ExportOrchestrator bullet/description
(ExportOrchestrator is a concrete orchestrator already documented under
Modules). Ensure the Seams section contains only true seams (abstract interfaces
with multiple adapters) and leave the Modules section entry for
ExportOrchestrator unchanged.

Source: Coding guidelines

QList<QueryResult> results;
for (const auto &raw: statements) {
const QString sql = raw.trimmed().replace('\n', "", Qt::CaseInsensitive);
const QString sql = raw.trimmed().replace('\n', "");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace newline stripping with whitespace normalization.

At Line 15, replacing '\n' with "" can merge tokens and break valid multiline SQL coming from the editor (SELECT ...\nFROM ... becomes SELECT ...FROM ... without spacing).

Suggested fix
-        const QString sql = raw.trimmed().replace('\n', "");
+        QString sql = raw.trimmed();
+        sql.replace('\r', ' ');
+        sql.replace('\n', ' ');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const QString sql = raw.trimmed().replace('\n', "");
QString sql = raw.trimmed();
sql.replace('\r', ' ');
sql.replace('\n', ' ');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/queryexecutor.cpp` at line 15, The current assignment to QString
sql uses raw.trimmed().replace('\n', "") which removes newlines and can glue
tokens; change it to normalize whitespace instead: replace newlines and other
whitespace runs with a single space and trim the result so multiline SQL like
"SELECT ...\nFROM ..." becomes "SELECT ... FROM ...". Update the expression that
sets sql (the line creating QString sql from raw) to perform a regex-based
whitespace collapse (e.g., replace sequences of whitespace characters with a
single space) and then call trimmed() so you preserve token separation while
still removing excess whitespace.

}

void ExportOrchestrator::onExportComplete() {
tearingDown_ = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset tearingDown_ when starting a new export.

Line 39 latches tearingDown_ = true, but runExport never clears it. Subsequent exports can short-circuit polling/completion checks.

Suggested fix
 void ExportOrchestrator::runExport(IDatabase *db, DatabaseInfo info,
                                    std::function<void(IDatabase *, const DatabaseInfo &, const CancellationToken *, ExportDataProgress *)> fn) {
+    tearingDown_ = false;
     progress_ = std::make_unique<ExportDataProgress>();
     tcs_ = std::make_unique<CancellationTokenSource>();
     completed_ = false;

Also applies to: 64-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/exportorchestrator.cpp` at line 39, The field tearingDown_ is set to
true during teardown but never reset, causing subsequent calls to runExport to
short-circuit; update the export startup logic (e.g., at the top of runExport or
wherever a new export job is initialized) to explicitly set tearingDown_ = false
before proceeding, and ensure any early-return paths that begin a fresh export
also reset tearingDown_; check the teardown path where tearingDown_ is set true
(the existing assignment) remains but mirror it with the reset at export start
so polling/completion logic sees the correct state.

Comment thread src/gui/mainwindow.cpp
Comment on lines +358 to 364
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();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle additional schema-changing SQL keywords in refresh trigger.

ALTER/RENAME scripts won’t trigger analyzeDatabase(), so tree/schema views can remain stale after successful execution.

Suggested patch
-    if (script.contains("create", Qt::CaseInsensitive) ||
+    if (script.contains("create", Qt::CaseInsensitive) ||
+        script.contains("alter", Qt::CaseInsensitive) ||
+        script.contains("rename", Qt::CaseInsensitive) ||
         script.contains("drop", Qt::CaseInsensitive) ||
         script.contains("insert", Qt::CaseInsensitive) ||
         script.contains("delete", Qt::CaseInsensitive)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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();
}
if (script.contains("create", Qt::CaseInsensitive) ||
script.contains("alter", Qt::CaseInsensitive) ||
script.contains("rename", Qt::CaseInsensitive) ||
script.contains("drop", Qt::CaseInsensitive) ||
script.contains("insert", Qt::CaseInsensitive) ||
script.contains("delete", Qt::CaseInsensitive)) {
ui->queryResultTab->setCurrentIndex(1);
analyzeDatabase();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/mainwindow.cpp` around lines 358 - 364, The refresh trigger only
looks for "create", "drop", "insert", "delete" in the SQL string, so
ALTER/RENAME statements don't call analyzeDatabase(); update the conditional
that examines the variable script (the same block that calls
ui->queryResultTab->setCurrentIndex(1) and analyzeDatabase()) to also detect
"alter" and "rename" (case-insensitive) so schema-changing statements trigger ui
refresh; keep the same behavior of switching to the results tab before calling
analyzeDatabase().

Comment thread src/gui/mainwindow.cpp
Comment on lines 396 to +401
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Connect export signals before starting export work.

connect(...) currently runs after exportToSql/exportToCsv(...). If export emits completion/progress immediately (fast path or early failure), slots can miss signals and leave actions disabled or status stale.

Suggested patch
-    exportOrchestrator->exportToSql(std::move(info), filepath, database.get());
-
     connect(exportOrchestrator.get(), &ExportOrchestrator::exportProgress,
             this, &MainWindow::onExportProgress, Qt::UniqueConnection);
     connect(exportOrchestrator.get(), &ExportOrchestrator::exportCompleted,
             this, &MainWindow::onExportCompleted, Qt::UniqueConnection);
+
+    exportOrchestrator->exportToSql(std::move(info), filepath, database.get());
@@
-    exportOrchestrator->exportToCsv(std::move(info), outputFolder, delimiter, database.get());
-
     connect(exportOrchestrator.get(), &ExportOrchestrator::exportProgress,
             this, &MainWindow::onExportProgress, Qt::UniqueConnection);
     connect(exportOrchestrator.get(), &ExportOrchestrator::exportCompleted,
             this, &MainWindow::onExportCompleted, Qt::UniqueConnection);
+
+    exportOrchestrator->exportToCsv(std::move(info), outputFolder, delimiter, database.get());

Also applies to: 418-423

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/mainwindow.cpp` around lines 396 - 401, Move the connect calls to
attach ExportOrchestrator signals before invoking export work so slots can't
miss immediate progress/completion; specifically, for calls like
exportOrchestrator->exportToSql(std::move(info), filepath, database.get()) and
exportOrchestrator->exportToCsv(...), call connect(exportOrchestrator.get(),
&ExportOrchestrator::exportProgress, this, &MainWindow::onExportProgress,
Qt::UniqueConnection) and connect(exportOrchestrator.get(),
&ExportOrchestrator::exportCompleted, this, &MainWindow::onExportCompleted,
Qt::UniqueConnection) first, then start the export; ensure you preserve the
std::move(info) semantics and apply the same reorder for the other block
(exportToCsv).

Comment on lines 43 to 47
auto model = std::make_unique<QStandardItemModel>(
static_cast<int>(result.rows.size()),
static_cast<int>(result.columns.size()),
container.get());
nullptr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/gui/queryresultpresenter.cpp"

# Ensure file exists and show relevant surrounding code
ls -la "$FILE"

# Show the region around the reported lines
sed -n '1,140p' "$FILE" | nl -ba | sed -n '1,140p'

# Specifically extract around the snippet and suggested fix context (lines ~35-110 in file)
sed -n '35,110p' "$FILE" | nl -ba

# Search for present(), presentToView(), setModel(), setParent(), release()/get()
rg -n "presentToView|present\s*\(|setModel\(|setParent\(|release\(|unique_ptr<|QStandardItemModel" "$FILE"

Repository: christianhelle/sqlitequery

Length of output: 214


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/gui/queryresultpresenter.cpp"

echo "== Exists =="
test -f "$FILE" && echo "found $FILE"

echo "== First 160 lines (with line numbers via awk) =="
awk '{printf "%5d  %s\n", NR, $0}' "$FILE" | sed -n '1,160p'

echo "== Focus around reported area (lines 25-120) =="
awk '{printf "%5d  %s\n", NR, $0}' "$FILE" | sed -n '25,120p'

echo "== Search for model ownership / setModel / setParent / release usage =="
rg -n "presentToView|present\s*\(|setModel\(|setParent\(|release\(|unique_ptr<|QStandardItemModel" "$FILE"

Repository: christianhelle/sqlitequery

Length of output: 7689


present() leaks QStandardItemModel instances (models have no parent and aren’t deleted when QTableViews are destroyed).

In QueryResultPresenter::present(), the model is created with a nullptr QObject parent (lines 43-46) and then ownership is released into tablePtr->setModel(model.release()) (lines 59-61). When clear()/present() later qDeleteAll(this->tableViews...), the QTableView deletion does not free the unparented QStandardItemModel, so repeated calls accumulate leaked models.

presentToView() is different: it sets model->setParent(view) (lines 93-94), so that path doesn’t have the same leak.

Suggested fix
-        auto model = std::make_unique<QStandardItemModel>(
+        auto model = std::make_unique<QStandardItemModel>(
             static_cast<int>(result.rows.size()),
             static_cast<int>(result.columns.size()),
             nullptr);
@@
         const auto tablePtr = new QTableView(container.get());
+        model->setParent(tablePtr);
         tablePtr->setModel(model.release());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/queryresultpresenter.cpp` around lines 43 - 47,
QueryResultPresenter::present() currently creates a QStandardItemModel with a
nullptr parent and then releases it to tablePtr->setModel(...), causing leaked
models; fix by giving the model a QObject parent tied to the QTableView before
releasing ownership (either construct the QStandardItemModel with tablePtr as
parent or call model->setParent(tablePtr) prior to model.release()), mirroring
presentToView() behavior so models are deleted with their view.

Comment on lines +73 to +75
if (view->model() != nullptr) {
view->setModel(nullptr);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

src/gui/queryresultpresenter.cpp (Lines 73-75): delete old model when refreshing the same view.
view->setModel(nullptr) only detaches; since the new model is parented to view (around Lines 93-94), repeated refreshes keep prior models alive as QObject children until view is destroyed.

Suggested fix
-    if (view->model() != nullptr) {
-        view->setModel(nullptr);
-    }
+    if (auto *oldModel = view->model(); oldModel != nullptr) {
+        view->setModel(nullptr);
+        if (oldModel->parent() == view) {
+            oldModel->deleteLater();
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gui/queryresultpresenter.cpp` around lines 73 - 75, In
QueryResultPresenter (around the view refresh code), the existing code only
calls view->setModel(nullptr) which detaches but does not delete the previous
model (the new models are parented to view later around the create-model logic),
causing accumulated QObject children; fix by explicitly deleting the old model
before detaching—e.g., if (auto old = view->model()) { delete old;
view->setModel(nullptr); }—so replace the current view->setModel(nullptr) branch
to delete view->model() first to prevent leaks.

@christianhelle
christianhelle deleted the improve-codebase-architecture branch June 10, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant