From 49f6619a4e8b702a02a4d30d2a99d52fef13f68c Mon Sep 17 00:00:00 2001 From: eelke Date: Sat, 17 Oct 2026 16:21:22 +0200 Subject: [PATCH 1/9] Switch to cmake + vcpkg, builds on linux, windows needs verification. Code has been modified to be portable. --- CMakeLists.txt | 30 +++ README.md | 38 ++++ core/CMakeLists.txt | 56 +++--- pglab/CMakeLists.txt | 164 +++++++++------ pglab/CreateDatabaseDialog.h | 2 +- pglab/CreateDatabaseDialog.ui | 2 +- pglab/catalog/models/DatabasesTableModel.cpp | 2 +- pglab/catalog/models/IndexModel.cpp | 2 +- pglab/catalog/models/SequenceModel.cpp | 12 +- pglab/catalog/tables/TablesTableModel.cpp | 8 +- pglab/crud/CrudModel.cpp | 2 +- pglab/querytool/QueryExplainModel.cpp | 2 +- pglablib/ASyncDBConnection.cpp | 200 ++++++++++--------- pglablib/ASyncDBConnection.h | 3 +- pglablib/CMakeLists.txt | 187 +++++++++++++++++ pglablib/SqlFormattingUtils.cpp | 2 +- pglablib/WaitHandleList.cpp | 31 --- pglablib/WaitHandleList.h | 26 --- pglablib/Win32Event.h | 47 ----- pglablib/catalog/PgLanguage.cpp | 6 +- pglablib/catalog/PgProc.cpp | 10 +- pglablib/catalog/PgServerObject.cpp | 2 +- pglablib/catalog/PgServerObject.h | 6 +- pglablib/codebuilder/StringEscapeRule.h | 1 - pglablib/utils/HumanReadableBytes.h | 1 + pglablib/utils/PasswordManager.cpp | 6 +- pglablib/utils/PasswordManager.h | 6 +- pgsql/ArrayParser.cpp | 1 + pgsql/CMakeLists.txt | 48 +++++ tests/CMakeLists.txt | 5 + tests/PgsqlTests/CMakeLists.txt | 24 +++ tests/mygtestutils/CMakeLists.txt | 19 ++ tests/pglabtests/CMakeLists.txt | 41 ++++ vcpkg.json | 13 ++ 34 files changed, 678 insertions(+), 327 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 pglablib/CMakeLists.txt delete mode 100644 pglablib/WaitHandleList.cpp delete mode 100644 pglablib/WaitHandleList.h delete mode 100644 pglablib/Win32Event.h create mode 100644 pgsql/CMakeLists.txt create mode 100644 tests/CMakeLists.txt create mode 100644 tests/PgsqlTests/CMakeLists.txt create mode 100644 tests/mygtestutils/CMakeLists.txt create mode 100644 tests/pglabtests/CMakeLists.txt create mode 100644 vcpkg.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a172074 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.21) + +project(pgLab LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +add_compile_definitions(BOOST_ENABLE_ASSERT_HANDLER QT_DEPRECATED_WARNINGS) +if(WIN32) + add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX) +endif() + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Sql Concurrent) + +option(BUILD_TESTS "Build the gtest executables and register them with ctest" ON) + +add_subdirectory(core) +add_subdirectory(pgsql) +add_subdirectory(pglablib) +add_subdirectory(pglab) + +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/README.md b/README.md index bdf5ea0..7c2eb7e 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,41 @@ The release notes use collected from git using [reno](https://docs.openstack.org - googletest - libpq +Boost, Botan, libpq and googletest are provided by vcpkg through the manifest in `vcpkg.json`, so you do not install them manually. + +## Required tools + +- CMake 3.21 or newer +- A C++20 compiler (GCC or Clang on Linux, Visual Studio 2022 on Windows) +- Qt 6 with the Core, Gui, Widgets, Sql and Concurrent components +- vcpkg, with the `VCPKG_ROOT` environment variable pointing to its checkout +- Ninja or make (optional, for faster builds) +- A PostgreSQL server (only needed to run the application against a database) + +## Building + +Configure with the vcpkg toolchain file. The first configure downloads and builds the dependencies, which takes a while: + +``` +cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake +cmake --build build +``` + +To skip the test executables, add `-DBUILD_TESTS=OFF` to the configure command. + +## Running the tests + +From the build directory, run: + +``` +ctest --output-on-failure +``` + +## Running the application + +The executable is built to `build/pglab/pglab`. On a machine without a display, set `QT_QPA_PLATFORM=offscreen`: + +``` +./build/pglab/pglab +``` + diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 95b09fc..ec63cf3 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,41 +1,31 @@ -set(CMAKE_AUTOMOC ON) - add_library(core STATIC - BackupFormatModel.cpp - CsvWriter.cpp my_boost_assert_handler.cpp - PasswordManager.cpp - SqlLexer.cpp) - + SqlLexer.cpp + CsvWriter.cpp + BackupFormatModel.cpp + ExplainTreeModelItem.cpp + jsoncpp.cpp + SqlLexer.h + ScopeGuard.h + CsvWriter.h + BackupFormatModel.h + Expected.h + ExplainTreeModelItem.h + json/json.h + std_utils.h + IntegerRange.h + ) + target_link_libraries(core PUBLIC - boost - botan - Qt5::Core + Qt6::Core ) - -target_include_directories(core INTERFACE + +target_include_directories(core PUBLIC $ -) + ) -set_target_properties(core PROPERTIES - CXX_STANDARD 14 +set_target_properties(core PROPERTIES + CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON - POSITION_INDEPENDENT_CODE True + POSITION_INDEPENDENT_CODE ON ) - -add_executable(runtests - test/main.cpp - test/tst_CsvWriter.cpp - test/tst_expected.cpp - test/tst_PasswordManager.cpp - test/tst_scopeguard.cpp - test/tst_SqlLexer.cpp - ) - -target_link_libraries(runtests - core - gtest - Threads::Threads - ) - -add_test(tests runtests) diff --git a/pglab/CMakeLists.txt b/pglab/CMakeLists.txt index d6a1072..ab4dda5 100644 --- a/pglab/CMakeLists.txt +++ b/pglab/CMakeLists.txt @@ -1,69 +1,115 @@ -cmake_minimum_required(VERSION 3.2) -project(pglab) +set(PGLAB_SOURCES + main.cpp + About.cpp + catalog/delegates/IconColumnDelegate.cpp + catalog/models/BaseTableModel.cpp + catalog/models/ColumnTableModel.cpp + catalog/models/ConstraintModel.cpp + catalog/models/DatabasesTableModel.cpp + catalog/models/DependantsTableModel.cpp + catalog/models/IndexModel.cpp + catalog/models/ProcTableModel.cpp + catalog/models/RolesTableModel.cpp + catalog/models/SequenceModel.cpp + catalog/models/TriggerTableModel.cpp + catalog/tables/TablesTableModel.cpp + catalog/widgets/CatalogConstraintPage.cpp + catalog/widgets/CatalogFunctionsPage.cpp + catalog/widgets/CatalogIndexPage.cpp + catalog/widgets/CatalogInspector.cpp + catalog/widgets/CatalogNamespacePage.cpp + catalog/widgets/CatalogPageBase.cpp + catalog/widgets/CatalogSequencesPage.cpp + catalog/widgets/CatalogTablesPage.cpp + catalog/widgets/CatalogTypesPage.cpp + catalog/widgets/ColumnPage.cpp + catalog/widgets/DependantsPage.cpp + catalog/widgets/TriggerPage.cpp + codeeditor/CodeEditor.cpp + codeeditor/EditorGutter.cpp + codeeditor/GutterPainter.cpp + crud/CrudModel.cpp + crud/CrudTab.cpp + querytool/QueryExplainModel.cpp + querytool/QueryParamListController.cpp + querytool/QueryResultModel.cpp + querytool/QueryTool.cpp + serverinspector/DatabasesPage.cpp + serverinspector/RolesPage.cpp + serverinspector/ServerInspector.cpp + util/Colors.cpp + util/PgLabItemDelegate.cpp + util/PgLabTableView.cpp + util/SqlSyntaxHighlighter.cpp + ConnectionConfigurationWidget.cpp + ConnectionController.cpp + ManagedPage.cpp + PgDumpOutputHighlighter.cpp + CreateDatabaseDialog.cpp + ConnectionManagerWindow.cpp + ConnectionListModel.cpp + SslModeModel.cpp + stopwatch.cpp + tuplesresultwidget.cpp + BackupDialog.cpp + MasterController.cpp + ParamTypeDelegate.cpp + OpenDatabase.cpp + ProcessStdioWidget.cpp + ResultTableModelUtil.cpp + NamespaceItemModel.cpp + PropertyProxyModel.cpp + CodeGenerator.cpp + UserConfiguration.cpp + widgets/SqlCodePreview.cpp + CustomFilterSortModel.cpp + PropertiesPage.cpp + PasswordPromptDialog.cpp + EditTableWidget.cpp + EditColumnTableModel.cpp + DatabaseWindow.cpp + widgets/SingleRecordModel.cpp + widgets/SingleRecordWidget.cpp + ) -set(CMAKE_AUTOMOC ON) -set(CMAKE_AUTOUIC ON) -set(CMAKE_AUTORCC ON) +set(PGLAB_FORMS + ConnectionManagerWindow.ui + CreateDatabaseDialog.ui + DatabaseWindow.ui + tuplesresultwidget.ui + querytool/QueryTab.ui + ProcessStdioWidget.ui + crud/CrudTab.ui + CodeGenerator.ui + widgets/SingleRecordWidget.ui + ) add_executable(pglab - ASyncDBConnection.cpp - ASyncWindow.cpp - BackupDialog.cpp - BackupRestore.cpp - CodeBuilderConfiguration.cpp - ConnectionConfig.cpp - ConnectionList.cpp - ConnectionListModel.cpp - ConnectionManagerWindow.cpp - DatabaseInspectorWidget.cpp - DatabasesTableModel.cpp - DatabaseWindow.cpp - ExplainTreeModelItem.cpp - GlobalIoService.cpp - jsoncpp.cpp - main.cpp - MainWindow.cpp - MasterController.cpp - OpenDatabase.cpp - ParamListModel.cpp - ParamTypeDelegate.cpp - PgAuthIdContainer.cpp - PgAuthId.cpp - PgClass.cpp - PgDatabaseCatalogue.cpp - PgDatabaseContainer.cpp - PgDatabase.cpp - PgNamespace.cpp - PgTypeContainer.cpp - PgType.cpp - ProcessStdioWidget.cpp - QueryExplainModel.cpp - QueryResultModel.cpp - QueryTab.cpp - RolesTableModel.cpp - ServerWindow.cpp - SqlSyntaxHighlighter.cpp - stopwatch.cpp - tsqueue.cpp - tuplesresultwidget.cpp - TypeSelectionItemModel.cpp - util.cpp + ${PGLAB_SOURCES} + ${PGLAB_FORMS} resources.qrc ) -set_target_properties(pglab PROPERTIES - CXX_STANDARD 14 - CXX_STANDARD_REQUIRED ON) - -if(CMAKE_COMPILER_IS_GNUCXX) - # target_compile_options(pglab PRIVATE -Wall -fpic -march=native ) - # set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og") +if(WIN32) + set_target_properties(pglab PROPERTIES WIN32_EXECUTABLE ON) endif() -target_link_libraries( pglab - core +target_link_libraries(pglab PRIVATE + pglablib pgsql - boost-system - Qt5::Widgets - pthread + core + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Sql + Qt6::Concurrent + ) + +target_include_directories(pglab PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +set_target_properties(pglab PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON ) diff --git a/pglab/CreateDatabaseDialog.h b/pglab/CreateDatabaseDialog.h index bcc00af..74cb209 100644 --- a/pglab/CreateDatabaseDialog.h +++ b/pglab/CreateDatabaseDialog.h @@ -3,7 +3,7 @@ #include -#include "catalog\PgDatabase.h" +#include "catalog/PgDatabase.h" class PgDatabaseCatalog; diff --git a/pglab/CreateDatabaseDialog.ui b/pglab/CreateDatabaseDialog.ui index f17dbac..a16e077 100644 --- a/pglab/CreateDatabaseDialog.ui +++ b/pglab/CreateDatabaseDialog.ui @@ -148,7 +148,7 @@ CodeEditor QPlainTextEdit -
codeeditor\CodeEditor.h
+
codeeditor/CodeEditor.h
diff --git a/pglab/catalog/models/DatabasesTableModel.cpp b/pglab/catalog/models/DatabasesTableModel.cpp index ac10ec5..289eea2 100644 --- a/pglab/catalog/models/DatabasesTableModel.cpp +++ b/pglab/catalog/models/DatabasesTableModel.cpp @@ -169,7 +169,7 @@ QVariant DatabasesTableModel::getData(const QModelIndex &index) const break; case SizeCol: if (d.size.totalBytes >= 0) - v = d.size.totalBytes; + v = QVariant::fromValue(d.size.totalBytes); break; case AclCol: v = db.aclString(); diff --git a/pglab/catalog/models/IndexModel.cpp b/pglab/catalog/models/IndexModel.cpp index 7cee4e4..e04f715 100644 --- a/pglab/catalog/models/IndexModel.cpp +++ b/pglab/catalog/models/IndexModel.cpp @@ -114,7 +114,7 @@ QVariant IndexModel::getData(const QModelIndex &index) const break; case SizeCol: - v = dat.sizeBytes; + v = QVariant::fromValue(dat.sizeBytes); break; case ExplicitIndexCol: v = !dat.isSupportingIndex(); diff --git a/pglab/catalog/models/SequenceModel.cpp b/pglab/catalog/models/SequenceModel.cpp index 659348f..640b50c 100644 --- a/pglab/catalog/models/SequenceModel.cpp +++ b/pglab/catalog/models/SequenceModel.cpp @@ -102,12 +102,12 @@ QVariant SequenceModel::data(const QModelIndex &index, int role) const case NameCol: return seq.objectName(); case SchemaCol: return seq.nsName(); case OwnerCol: return seq.ownerName(); - case LastCol: return seq.last; - case StartCol: return seq.start; - case MinCol: return seq.min; - case MaxCol: return seq.max; - case IncrementCol: return seq.increment; - case CacheCol: return seq.cache; + case LastCol: return QVariant::fromValue(seq.last); + case StartCol: return QVariant::fromValue(seq.start); + case MinCol: return QVariant::fromValue(seq.min); + case MaxCol: return QVariant::fromValue(seq.max); + case IncrementCol: return QVariant::fromValue(seq.increment); + case CacheCol: return QVariant::fromValue(seq.cache); case CycledCol: return seq.cycled; case CalledCol: return seq.called; case AclCol: return seq.aclString(); diff --git a/pglab/catalog/tables/TablesTableModel.cpp b/pglab/catalog/tables/TablesTableModel.cpp index 0df8ff2..d6a4e8b 100644 --- a/pglab/catalog/tables/TablesTableModel.cpp +++ b/pglab/catalog/tables/TablesTableModel.cpp @@ -178,10 +178,10 @@ QVariant TablesTableModel::getData(const QModelIndex &index) const case OptionsCol: break; case AclCol: return t.aclString(); case CommentCol: return t.description; - case TotalSizeCol: return s.oid == 0 ? QVariant() : s.totalBytes; - case TableSizeCol: return s.oid == 0 ? QVariant() : s.totalBytes - s.indexBytes - s.toastBytes; - case IndexSizeCol: return s.oid == 0 ? QVariant() : s.indexBytes; - case ToastSizeCol: return s.oid == 0 ? QVariant() : s.toastBytes; + case TotalSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.totalBytes); + case TableSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.totalBytes - s.indexBytes - s.toastBytes); + case IndexSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.indexBytes); + case ToastSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.toastBytes); } return QVariant(); diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index d688bd6..8367f32 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -150,7 +150,7 @@ QVariant StringToVariant(const std::string &str, Oid oid) case Pgsql::oid_oid: case Pgsql::int8_oid: - return v.operator int64_t(); + return QVariant::fromValue(static_cast(v)); case Pgsql::bool_oid: return v.operator bool(); diff --git a/pglab/querytool/QueryExplainModel.cpp b/pglab/querytool/QueryExplainModel.cpp index 51381ec..bcce73b 100644 --- a/pglab/querytool/QueryExplainModel.cpp +++ b/pglab/querytool/QueryExplainModel.cpp @@ -40,7 +40,7 @@ if (role == Qt::DisplayRole) { result = item->estimateError(); break; case c_ColumnRowCount: - result = item->actualRows; + result = QVariant::fromValue(item->actualRows); break; case c_ColumnLoops: result = item->actualLoops; diff --git a/pglablib/ASyncDBConnection.cpp b/pglablib/ASyncDBConnection.cpp index 1c27fb9..2b5c2fb 100644 --- a/pglablib/ASyncDBConnection.cpp +++ b/pglablib/ASyncDBConnection.cpp @@ -1,13 +1,14 @@ -#include "ASyncDBConnection.h" +#include "ASyncDBConnection.h" #include "ScopeGuard.h" #include "util.h" #include "Pgsql_PgException.h" -#include "Win32Event.h" -#include "WaitHandleList.h" +#include #include #include -#include #include +#include +#include +#include namespace { @@ -45,10 +46,6 @@ public: struct t_Command { std::mutex m_mutex; t_CommandQueue m_queue; - Win32Event m_newEvent; - t_Command() - : m_newEvent(Win32Event::Reset::Auto, Win32Event::Initial::Clear) - {} } m_commandQueue; ConnectionConfig m_config; @@ -64,18 +61,29 @@ public: void stop(); + /// Wakes the worker thread when it is waiting in its event loop + void wake(); + private: + enum class WaitResult { + Socket, ///< the socket became ready + Wakeup, ///< woken up by stop() or by a new command + Timeout ///< the timeout expired + }; + ASyncDBConnection *asyncConnObject = nullptr; - /// \todo Implement new method to stop the thread - Win32Event m_stopEvent; + std::atomic terminateRequested{false}; ///< is set when the thread should stop + std::mutex m_wakeMutex; + QEventLoop *m_loop = nullptr; ///< loop the worker blocks in, guarded by m_wakeMutex Pgsql::Connection m_connection; - bool terminateRequested = false; ///< is set when the thread should stop bool m_terminated = true; Pgsql::Canceller m_canceller; QElapsedTimer m_timer; + bool hasQueuedCommand(); + WaitResult waitForSocket(QSocketNotifier::Type type, int timeout_ms, bool wakeOnNewCommand); bool makeConnection(); void communicate(); @@ -97,7 +105,6 @@ private: ASyncDBConnectionThread::ASyncDBConnectionThread(ASyncDBConnection *asco) : asyncConnObject(asco) - , m_stopEvent(Win32Event::Reset::Manual, Win32Event::Initial::Clear) {} void ASyncDBConnectionThread::run() @@ -154,6 +161,59 @@ bool ASyncDBConnectionThread::cancel() return m_canceller.cancel(nullptr); } +bool ASyncDBConnectionThread::hasQueuedCommand() +{ + std::lock_guard lg(m_commandQueue.m_mutex); + return !m_commandQueue.m_queue.empty(); +} + +ASyncDBConnectionThread::WaitResult ASyncDBConnectionThread::waitForSocket(QSocketNotifier::Type type, int timeout_ms, bool wakeOnNewCommand) +{ + QEventLoop loop; + WaitResult result = WaitResult::Wakeup; + + QSocketNotifier notifier(m_connection.socket(), type); + QObject::connect(¬ifier, &QSocketNotifier::activated, &loop, [&result, &loop] { + result = WaitResult::Socket; + loop.quit(); + }); + + QTimer timer; + timer.setSingleShot(true); + if (timeout_ms >= 0) { + QObject::connect(&timer, &QTimer::timeout, &loop, [&result, &loop] { + result = WaitResult::Timeout; + loop.quit(); + }); + timer.start(timeout_ms); + } + + { + std::lock_guard lg(m_wakeMutex); + m_loop = &loop; + } + + // Checked after publishing the loop, so a wake-up that happened before is not lost + bool pending = terminateRequested || (wakeOnNewCommand && hasQueuedCommand()); + if (!pending) { + loop.exec(); + } + + { + std::lock_guard lg(m_wakeMutex); + m_loop = nullptr; + } + return result; +} + +void ASyncDBConnectionThread::wake() +{ + std::lock_guard lg(m_wakeMutex); + if (m_loop) { + QMetaObject::invokeMethod(m_loop, [loop = m_loop] { loop->quit(); }, Qt::QueuedConnection); + } +} + bool ASyncDBConnectionThread::makeConnection() { using namespace std::literals::chrono_literals; @@ -165,30 +225,14 @@ bool ASyncDBConnectionThread::makeConnection() bool ok = m_connection.connectStart(conn_string); //keywords, values); auto start = std::chrono::steady_clock::now(); if (ok && m_connection.status() != CONNECTION_BAD) { - Win32Event socket_event(Win32Event::Reset::Auto, Win32Event::Initial::Clear); - - long fd = FD_WRITE; + QSocketNotifier::Type type = QSocketNotifier::Write; while (true) { // poll till complete or failed (we can get an abort command) - int sock = m_connection.socket(); - WSAEventSelect(sock, socket_event.handle(), fd); - WaitHandleList whl; - auto wait_result_socket_event = whl.add(socket_event); - auto wait_result_stop = whl.add(m_stopEvent); + auto elapsed = std::chrono::steady_clock::now() - start; + auto remaining = std::chrono::duration_cast(30s - elapsed).count(); + WaitResult res = waitForSocket(type, remaining > 0 ? static_cast(remaining) : 0, false); - auto nu = std::chrono::steady_clock::now(); - std::chrono::duration diff = -(nu-start); - diff += 30s; - DWORD timeout = diff.count(); - - DWORD res = MsgWaitForMultipleObjectsEx( - whl.count(), // _In_ DWORD nCount, - whl, // _In_ const HANDLE *pHandles, - timeout, // _In_ DWORD dwMilliseconds, - 0, // _In_ DWORD dwWakeMask, - 0 // _In_ DWORD dwFlags - ); - if (res == wait_result_socket_event) { + if (res == WaitResult::Socket) { auto poll_state = m_connection.connectPoll(); if (poll_state == PGRES_POLLING_OK) { qDebug() << "ASyncDBConnection Connected"; @@ -202,17 +246,21 @@ bool ASyncDBConnectionThread::makeConnection() } else if (poll_state == PGRES_POLLING_READING) { doStateCallback({ ASyncDBConnection::State::Connecting, "Negotiating" }); - fd = FD_READ; + type = QSocketNotifier::Read; } else if (poll_state == PGRES_POLLING_WRITING) { doStateCallback({ ASyncDBConnection::State::Connecting, "Negotiating" }); - fd = FD_WRITE; + type = QSocketNotifier::Write; } } - else if (res == wait_result_stop) { + else if (terminateRequested) { + m_connection.close(); + return false; + } + else if (res == WaitResult::Timeout) { + doStateCallback({ ASyncDBConnection::State::NotConnected, "Connection timed out" }); m_connection.close(); return false; - } } // end while (true) } @@ -252,7 +300,7 @@ void ASyncDBConnectionThread::communicate() void ASyncDBConnectionThread::stop() { terminateRequested = true; - m_stopEvent.set(); + wake(); } void ASyncDBConnectionThread::doStateCallback(ASyncDBConnection::StateData state) @@ -264,34 +312,18 @@ void ASyncDBConnectionThread::doStateCallback(ASyncDBConnection::StateData state bool ASyncDBConnectionThread::waitForAndSendCommand() { - SOCKET sock = static_cast(m_connection.socket()); - Win32Event socket_event(Win32Event::Reset::Manual, Win32Event::Initial::Clear); - long fd = FD_READ | FD_CLOSE; - WSAEventSelect(sock, socket_event.handle(), fd); - - WaitHandleList whl; - auto wait_result_new_command = whl.add(m_commandQueue.m_newEvent); - auto wait_result_socket = whl.add(socket_event); - whl.add(m_stopEvent); - - DWORD res = MsgWaitForMultipleObjectsEx( - whl.count(), // _In_ DWORD nCount, - whl, // _In_ const HANDLE *pHandles, - INFINITE, // _In_ DWORD dwMilliseconds, - 0, // _In_ DWORD dwWakeMask, - 0 // _In_ DWORD dwFlags - ); - if (res == wait_result_new_command) { + if (hasQueuedCommand()) { doNewCommand(); + return true; + } + + // Idle connection: a readable socket carries notifies or notices, or signals a lost connection + if (waitForSocket(QSocketNotifier::Read, -1, true) == WaitResult::Socket) { + if (!m_connection.consumeInput()) { + doStateCallback({ ASyncDBConnection::State::NotConnected, "Connection lost" }); + return false; + } } - else if (res == wait_result_socket) { - WSANETWORKEVENTS net_events; - WSAEnumNetworkEvents(sock, socket_event.handle(), &net_events); - if (net_events.lNetworkEvents & FD_CLOSE) { - doStateCallback({ ASyncDBConnection::State::NotConnected, "Connection lost" }); - return false; - } - } // Note if it was stop we can just return and function // above will stop looping because terminateRequested has been set too by stop return true; @@ -347,42 +379,16 @@ bool ASyncDBConnectionThread::consumeResultInput() void ASyncDBConnectionThread::waitForResult() { - SOCKET sock = static_cast(m_connection.socket()); - Win32Event socket_event(Win32Event::Reset::Manual, Win32Event::Initial::Clear); - - long fd = FD_READ | FD_CLOSE; - bool finished = false; while ( ! finished) { - WSAEventSelect(sock, socket_event.handle(), fd); + WaitResult res = waitForSocket(QSocketNotifier::Read, -1, false); - WaitHandleList whl; - auto wait_result_socket = whl.add(socket_event); - auto wait_result_stop = whl.add(m_stopEvent); - - DWORD res = MsgWaitForMultipleObjectsEx( - whl.count(), // _In_ DWORD nCount, - whl, // _In_ const HANDLE *pHandles, - INFINITE, // _In_ DWORD dwMilliseconds, - 0, // _In_ DWORD dwWakeMask, - 0 // _In_ DWORD dwFlags - ); - if (res == wait_result_socket) { - WSANETWORKEVENTS net_events; - WSAEnumNetworkEvents(sock, socket_event.handle(), &net_events); - - if (net_events.lNetworkEvents & FD_READ) { - if (consumeResultInput()) { - finished = true; - } + if (res == WaitResult::Socket) { + if (consumeResultInput()) { + finished = true; } - else if (net_events.lNetworkEvents & FD_CLOSE) { - doStateCallback({ ASyncDBConnection::State::NotConnected, "Close while waiting for result" }); - finished = true; - stop(); - } } - if (res == wait_result_stop) { + else if (terminateRequested) { // Send cancel, close connection and terminate thread cancel(); doStateCallback(ASyncDBConnection::State::Terminating); @@ -455,8 +461,8 @@ bool ASyncDBConnection::send(const std::string &command, on_result_callback on_r { std::lock_guard lg(m_threadData->m_commandQueue.m_mutex); m_threadData->m_commandQueue.m_queue.emplace(command, on_result); - m_threadData->m_commandQueue.m_newEvent.set(); } + m_threadData->wake(); return true; } @@ -468,8 +474,8 @@ bool ASyncDBConnection::send(const std::string &command, Pgsql::Params params, o { std::lock_guard lg(m_threadData->m_commandQueue.m_mutex); m_threadData->m_commandQueue.m_queue.emplace(command, std::move(params), on_result); - m_threadData->m_commandQueue.m_newEvent.set(); } + m_threadData->wake(); return true; } diff --git a/pglablib/ASyncDBConnection.h b/pglablib/ASyncDBConnection.h index 02f5310..0479f5f 100644 --- a/pglablib/ASyncDBConnection.h +++ b/pglablib/ASyncDBConnection.h @@ -11,6 +11,7 @@ #include #include #include +#include class ASyncDBConnectionThread; /** \brief Class that handles asynchronous execution of queries. @@ -33,7 +34,7 @@ public: class StateData { public: - State State; + enum State State; QString Message; StateData(enum State state, QString message = QString()) diff --git a/pglablib/CMakeLists.txt b/pglablib/CMakeLists.txt new file mode 100644 index 0000000..a3c759d --- /dev/null +++ b/pglablib/CMakeLists.txt @@ -0,0 +1,187 @@ +add_library(pglablib STATIC + Pglablib.cpp + ASyncDBConnection.cpp + ConnectionConfig.cpp + TestConnection.cpp + catalog/PgType.cpp + catalog/PgTypeContainer.cpp + catalog/PgNamespace.cpp + catalog/PgClass.cpp + catalog/PgDatabase.cpp + catalog/PgDatabaseContainer.cpp + catalog/PgAuthId.cpp + catalog/PgAuthIdContainer.cpp + catalog/PgClassContainer.cpp + catalog/PgDatabaseCatalog.cpp + catalog/PgNamespaceContainer.cpp + catalog/PgAttribute.cpp + catalog/PgContainer.cpp + catalog/PgAttributeContainer.cpp + catalog/PgIndex.cpp + catalog/PgIndexContainer.cpp + catalog/PgConstraint.cpp + catalog/PgConstraintContainer.cpp + ParamListJson.cpp + ParamListModel.cpp + sqlite/SQLiteConnection.cpp + sqlite/sqlite3.c + ui/catalog/tables/TableNode.cpp + ui/catalog/tables/TableSize.cpp + ui/catalog/tables/TableTreeBuilder.cpp + util.cpp + SqlFormattingUtils.cpp + catalog/PgKeywordList.cpp + QueryGenerator.cpp + catalog/PgObject.cpp + catalog/PgTablespace.cpp + catalog/PgTablespaceContainer.cpp + codebuilder/LanguageConfig.cpp + codebuilder/CodeBuilder.cpp + codebuilder/NameManglingRules.cpp + codebuilder/DefaultConfigs.cpp + codebuilder/TypeMappings.cpp + codebuilder/IndentationConfig.cpp + codebuilder/StructureTemplate.cpp + FormatToStream.cpp + codebuilder/StringLiteralRules.cpp + codebuilder/StringEscapeRule.cpp + catalog/PgTrigger.cpp + catalog/PgTriggerContainer.cpp + catalog/PgProc.cpp + catalog/PgProcContainer.cpp + catalog/PgDatabaseObject.cpp + catalog/PgServerObject.cpp + catalog/PgNamespaceObject.cpp + catalog/PgCollation.cpp + catalog/PgCollationContainer.cpp + catalog/PgInherits.cpp + catalog/PgInheritsContainer.cpp + SelectionEditorFactory.cpp + catalog/PgAm.cpp + model/TypeModelFactory.cpp + model/TypeSelectionItemModel.cpp + catalog/PgAmContainer.cpp + model/CollationModel.cpp + model/CollationModelFactory.cpp + catalog/PgLanguageContainer.cpp + catalog/PgLanguage.cpp + catalog/PgAcl.cpp + catalog/PgSequence.cpp + catalog/PgSequenceContainer.cpp + utils/HumanReadableBytes.cpp + utils/KeyStrengthener.cpp + utils/PasswordManager.cpp + utils/PostgresqlUrlParser.cpp + Pglablib.h + ASyncDBConnection.h + ConnectionConfig.h + TestConnection.h + catalog/PgType.h + catalog/PgTypeContainer.h + catalog/PgNamespace.h + catalog/PgClass.h + catalog/PgDatabase.h + catalog/PgDatabaseContainer.h + catalog/PgContainer.h + catalog/PgAuthId.h + catalog/PgAuthIdContainer.h + catalog/PgClassContainer.h + catalog/PgDatabaseCatalog.h + catalog/PgNamespaceContainer.h + catalog/PgAttribute.h + catalog/PgAttributeContainer.h + catalog/PgIndex.h + catalog/PgIndexContainer.h + catalog/PgConstraint.h + catalog/PgConstraintContainer.h + ParamListJson.h + ParamListModel.h + sqlite/SQLiteConnection.h + sqlite/sqlite3.h + sqlite/sqlite3ext.h + ui/catalog/tables/TableNode.h + ui/catalog/tables/TableSize.h + ui/catalog/tables/TableTreeBuilder.h + util.h + SqlFormattingUtils.h + catalog/PgCatalogTypes.h + catalog/PgKeywordList.h + QueryGenerator.h + catalog/PgObject.h + catalog/PgTablespace.h + catalog/PgTablespaceContainer.h + codebuilder/LanguageConfig.h + codebuilder/CodeBuilder.h + codebuilder/NameManglingRules.h + codebuilder/DefaultConfigs.h + codebuilder/TypeMappings.h + codebuilder/IndentationConfig.h + codebuilder/StructureTemplate.h + FormatToStream.h + codebuilder/ResultLoopTemplate.h + codebuilder/StringEscapeRule.h + codebuilder/StringLiteralRules.h + catalog/PgTrigger.h + catalog/PgTriggerContainer.h + catalog/PgProc.h + catalog/PgProcContainer.h + catalog/PgDatabaseObject.h + catalog/PgServerObject.h + catalog/PgNamespaceObject.h + catalog/PgCollation.h + catalog/PgCollationContainer.h + AbstractModelFactory.h + AbstractEditorFactory.h + catalog/PgInherits.h + catalog/PgInheritsContainer.h + SelectionEditorFactory.h + model/TypeSelectionItemModel.h + model/TypeModelFactory.h + catalog/PgAm.h + catalog/PgAmContainer.h + model/CollationModel.h + model/CollationModelFactory.h + catalog/PgLanguageContainer.h + catalog/PgLanguage.h + catalog/PgAcl.h + catalog/PgSequence.h + catalog/PgSequenceContainer.h + utils/HumanReadableBytes.h + utils/KeyStrengthener.h + utils/PasswordManager.h + utils/PostgresqlUrlParser.h + ) + +find_package(Boost REQUIRED COMPONENTS container timer chrono assert) + +find_path(BOTAN_INCLUDE_DIR botan/hash.h REQUIRED) +find_library(BOTAN_LIBRARY NAMES botan-3 botan REQUIRED) +if(NOT TARGET Botan::Botan) + add_library(Botan::Botan STATIC IMPORTED) + set_target_properties(Botan::Botan PROPERTIES + IMPORTED_LOCATION "${BOTAN_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${BOTAN_INCLUDE_DIR}" + ) +endif() + +target_link_libraries(pglablib PUBLIC + core + pgsql + Boost::container + Boost::timer + Boost::chrono + Boost::assert + Botan::Botan + Qt6::Core + Qt6::Widgets + ) + +target_include_directories(pglablib PUBLIC + $ + ) + +set_target_properties(pglablib PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) diff --git a/pglablib/SqlFormattingUtils.cpp b/pglablib/SqlFormattingUtils.cpp index a75620e..ff7f1d3 100644 --- a/pglablib/SqlFormattingUtils.cpp +++ b/pglablib/SqlFormattingUtils.cpp @@ -200,7 +200,7 @@ QString quoteIdent(QString ident) { assert(ident.length() > 0); - static const wchar_t dquote = L'"'; + static const QChar dquote = u'"'; if (identNeedsQuotes(ident)) { QString out; diff --git a/pglablib/WaitHandleList.cpp b/pglablib/WaitHandleList.cpp deleted file mode 100644 index d219610..0000000 --- a/pglablib/WaitHandleList.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "WaitHandleList.h" -#include "win32event.h" - -WaitHandleList::WaitHandleList() = default; -WaitHandleList::~WaitHandleList() = default; - -WaitResultValue WaitHandleList::add(HANDLE h) -{ - m_waitHandles.push_back(h); - return WAIT_OBJECT_0 + static_cast(m_waitHandles.size() - 1); -} - -WaitResultValue WaitHandleList::add(Win32Event &e) -{ - return add(e.handle()); -} - -DWORD WaitHandleList::count() const -{ - return static_cast(m_waitHandles.size()); -} - -void WaitHandleList::clear() -{ - m_waitHandles.clear(); -} - -WaitHandleList::operator const HANDLE*() const -{ - return m_waitHandles.data(); -} diff --git a/pglablib/WaitHandleList.h b/pglablib/WaitHandleList.h deleted file mode 100644 index 376e817..0000000 --- a/pglablib/WaitHandleList.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef WAITHANDLELIST_H -#define WAITHANDLELIST_H - -#include -#include - -class Win32Event; - -using WaitResultValue = DWORD; - -class WaitHandleList { -public: - WaitHandleList(); - ~WaitHandleList(); - - WaitResultValue add(HANDLE h); - WaitResultValue add(Win32Event &e); - DWORD count() const; - void clear(); - operator const HANDLE*() const; - -private: - std::vector m_waitHandles; -}; - -#endif // WAITHANDLELIST_H diff --git a/pglablib/Win32Event.h b/pglablib/Win32Event.h deleted file mode 100644 index 684d13f..0000000 --- a/pglablib/Win32Event.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef WIN32EVENT_H -#define WIN32EVENT_H - -#include - -#include -/** Simpel wrapper around a Win32 Event object. - -Mostly to make cleanup automatic.*/ -class Win32Event { -public: - enum class Reset { Auto=0, Manual=1 }; - enum class Initial { Clear=0, Set=1 }; - - Win32Event(Reset r, Initial is) - : hEvent(CreateEvent( - nullptr, // _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, - BOOL(r), // _In_ BOOL bManualReset, - BOOL(is), // _In_ BOOL bInitialState, - nullptr //_In_opt_ LPCTSTR lpName - )) - {} - - Win32Event(Reset r, Initial is, int sock, long net_events) - : Win32Event(r, is) - { - WSAEventSelect(sock, hEvent, net_events); - } - - ~Win32Event() - { - CloseHandle(hEvent); - } - - Win32Event(const Win32Event &) = delete; - Win32Event &operator=(const Win32Event &) = delete; - - void set() { SetEvent(hEvent); } - - void reset() { ResetEvent(hEvent); } - - HANDLE handle() { return hEvent; } -private: - HANDLE hEvent; -}; - -#endif // WIN32EVENT_H diff --git a/pglablib/catalog/PgLanguage.cpp b/pglablib/catalog/PgLanguage.cpp index cd9dd4f..c224d23 100644 --- a/pglablib/catalog/PgLanguage.cpp +++ b/pglablib/catalog/PgLanguage.cpp @@ -1,13 +1,15 @@ #include "PgLanguage.h" +#include + QString PgLanguage::createSql() const { - throw std::exception("PgLanguage::createSql() not implemented"); + throw std::logic_error("PgLanguage::createSql() not implemented"); } QString PgLanguage::dropSql() const { - throw std::exception("PgLanguage::dropSql() not implemented"); + throw std::logic_error("PgLanguage::dropSql() not implemented"); } QString PgLanguage::typeName() const diff --git a/pglablib/catalog/PgProc.cpp b/pglablib/catalog/PgProc.cpp index 83fe734..02cf6d0 100644 --- a/pglablib/catalog/PgProc.cpp +++ b/pglablib/catalog/PgProc.cpp @@ -48,15 +48,15 @@ namespace { for(; index < len; index++) { QChar ch = str[index]; - if (!double_quote && ch == L'\'') + if (!double_quote && ch == u'\'') single_quote = !single_quote; - else if (!single_quote && ch == L'"') + else if (!single_quote && ch == u'"') double_quote = !double_quote; - else if (!double_quote && !single_quote && ch == L'(') + else if (!double_quote && !single_quote && ch == u'(') brackets++; - else if (!double_quote && !single_quote && ch == L')') + else if (!double_quote && !single_quote && ch == u')') brackets--; - else if (!double_quote && !single_quote && brackets == 0 && ch == L',') { + else if (!double_quote && !single_quote && brackets == 0 && ch == u',') { if (index != start_array) res.push_back(str.mid(start_array, index - 1).trimmed()); else diff --git a/pglablib/catalog/PgServerObject.cpp b/pglablib/catalog/PgServerObject.cpp index f85bd1f..1108917 100644 --- a/pglablib/catalog/PgServerObject.cpp +++ b/pglablib/catalog/PgServerObject.cpp @@ -38,7 +38,7 @@ QString PgServerObject::alterOwnerSql(const QString& ident) const } -void PgServerObject::setAcls(boost::optional acls) +void PgServerObject::setAcls(std::optional acls) { m_acls = std::move(acls); } diff --git a/pglablib/catalog/PgServerObject.h b/pglablib/catalog/PgServerObject.h index 5c08fd3..b1520a1 100644 --- a/pglablib/catalog/PgServerObject.h +++ b/pglablib/catalog/PgServerObject.h @@ -4,7 +4,7 @@ #include #include "PgObject.h" #include "PgAcl.h" -#include +#include class PgAuthId; @@ -26,7 +26,7 @@ public: * @brief setAcls * @param acls Important: pass empty optional when acl IS NULL, pass empty list for empty array */ - void setAcls(boost::optional acls); + void setAcls(std::optional acls); QString aclString() const; /** * @brief grantSql @@ -53,7 +53,7 @@ protected: private: Oid m_ownerOid = InvalidOid; const PgAuthId * m_owner = nullptr; - boost::optional m_acls; + std::optional m_acls; }; #endif // PGSERVEROBJECT_H diff --git a/pglablib/codebuilder/StringEscapeRule.h b/pglablib/codebuilder/StringEscapeRule.h index a8c9822..fb01dd8 100644 --- a/pglablib/codebuilder/StringEscapeRule.h +++ b/pglablib/codebuilder/StringEscapeRule.h @@ -2,7 +2,6 @@ #define STRINGESCAPERULE_H #include -#include enum class ConvertToNumericEscape { Utf8, ///< Generates upto four escapes for 4 utf8 chars diff --git a/pglablib/utils/HumanReadableBytes.h b/pglablib/utils/HumanReadableBytes.h index b76de05..b17ec0f 100644 --- a/pglablib/utils/HumanReadableBytes.h +++ b/pglablib/utils/HumanReadableBytes.h @@ -1,5 +1,6 @@ #pragma once +#include #include std::string HumanReadableBytes(uint64_t bytes); diff --git a/pglablib/utils/PasswordManager.cpp b/pglablib/utils/PasswordManager.cpp index b0a71de..dfa0476 100644 --- a/pglablib/utils/PasswordManager.cpp +++ b/pglablib/utils/PasswordManager.cpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include using namespace Botan; @@ -33,7 +33,7 @@ public: * distinct key which is derived by hashing the wrapped key name with HMAC. */ const std::vector wrapped_name = - nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()), + nist_key_wrap_padded(reinterpret_cast(name.data()), name.size(), *m_cipher); @@ -47,7 +47,7 @@ public: secure_vector get(const std::string& name, const std::string_view &wrapped_key) const { const std::vector wrapped_name = - nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()), + nist_key_wrap_padded(reinterpret_cast(name.data()), name.size(), *m_cipher); diff --git a/pglablib/utils/PasswordManager.h b/pglablib/utils/PasswordManager.h index 4bba574..8fb99d1 100644 --- a/pglablib/utils/PasswordManager.h +++ b/pglablib/utils/PasswordManager.h @@ -19,7 +19,11 @@ namespace Botan { class PasswordManagerException: public std::exception { public: - using std::exception::exception; //(char const* const _Message); + PasswordManagerException() = default; + explicit PasswordManagerException(const char *message) : m_message(message) {} + const char *what() const noexcept override { return m_message.c_str(); } +private: + std::string m_message; }; class PasswordManagerLockedException: public PasswordManagerException { diff --git a/pgsql/ArrayParser.cpp b/pgsql/ArrayParser.cpp index 1f29a61..cdb5999 100644 --- a/pgsql/ArrayParser.cpp +++ b/pgsql/ArrayParser.cpp @@ -1,4 +1,5 @@ #include "ArrayParser.h" +#include #include using namespace Pgsql; diff --git a/pgsql/CMakeLists.txt b/pgsql/CMakeLists.txt new file mode 100644 index 0000000..f6b0f39 --- /dev/null +++ b/pgsql/CMakeLists.txt @@ -0,0 +1,48 @@ +find_package(PostgreSQL REQUIRED) + +add_library(pgsql STATIC + Pgsql_Connection.cpp + Pgsql_Params.cpp + Pgsql_PgException.cpp + Pgsql_Result.cpp + Pgsql_Row.cpp + Pgsql_Value.cpp + ArrayParser.cpp + Pgsql_oids.cpp + Pgsql_Transaction.cpp + Pgsql_ErrorDetails.cpp + Pgsql_Canceller.cpp + Pgsql_Connection.h + Pgsql_IResult.h + Pgsql_Params.h + Pgsql_PgException.h + Pgsql_Result.h + Pgsql_ResultConstIterator.h + Pgsql_Row.h + Pgsql_Value.h + Pgsql_declare.h + Pgsql_Col.h + ArrayParser.h + Pgsql_oids.h + SqlGenerator.h + Pgsql_Transaction.h + Pgsql_ErrorDetails.h + Pgsql_Canceller.h + ) + +target_link_libraries(pgsql PUBLIC + core + PostgreSQL::PostgreSQL + Qt6::Core + Qt6::Widgets + ) + +target_include_directories(pgsql PUBLIC + $ + ) + +set_target_properties(pgsql PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..a02ee3c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(GTest CONFIG REQUIRED) + +add_subdirectory(mygtestutils) +add_subdirectory(PgsqlTests) +add_subdirectory(pglabtests) diff --git a/tests/PgsqlTests/CMakeLists.txt b/tests/PgsqlTests/CMakeLists.txt new file mode 100644 index 0000000..34071a1 --- /dev/null +++ b/tests/PgsqlTests/CMakeLists.txt @@ -0,0 +1,24 @@ +add_executable(PgsqlTests + main.cpp + tst_Value.cpp + tst_ArrayParser.cpp + tst_Pgsql_oids.cpp + ) + +target_link_libraries(PgsqlTests PRIVATE + pgsql + mygtestutils + GTest::gtest + Qt6::Core + ) + +target_include_directories(PgsqlTests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +set_target_properties(PgsqlTests PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + +add_test(NAME PgsqlTests COMMAND PgsqlTests) diff --git a/tests/mygtestutils/CMakeLists.txt b/tests/mygtestutils/CMakeLists.txt new file mode 100644 index 0000000..930a0ad --- /dev/null +++ b/tests/mygtestutils/CMakeLists.txt @@ -0,0 +1,19 @@ +add_library(mygtestutils STATIC + PrintTo_Qt.cpp + PrintTo_Qt.h + ) + +target_link_libraries(mygtestutils PUBLIC + GTest::gtest + Qt6::Core + ) + +target_include_directories(mygtestutils PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +set_target_properties(mygtestutils PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON + ) diff --git a/tests/pglabtests/CMakeLists.txt b/tests/pglabtests/CMakeLists.txt new file mode 100644 index 0000000..f13dc29 --- /dev/null +++ b/tests/pglabtests/CMakeLists.txt @@ -0,0 +1,41 @@ +add_executable(pglabtests + main.cpp + TableTreeBuilderTests.cpp + tst_ConvertLangToSqlString.cpp + tst_ConvertToMultiLineCString.cpp + tst_ExplainJsonParser.cpp + tst_HumanReadableBytes.cpp + tst_TryParsePostgresqlUrl.cpp + tst_escapeConnectionStringValue.cpp + tst_expected.cpp + tst_SqlLexer.cpp + tst_scopeguard.cpp + tst_CsvWriter.cpp + tst_PasswordManager.cpp + tst_ParamJson.cpp + tst_CodeBuilder.cpp + tst_NameManglingRules.cpp + tst_TypeMappings.cpp + tst_Range.cpp + ) + +target_link_libraries(pglabtests PRIVATE + pglablib + pgsql + core + mygtestutils + GTest::gtest + Qt6::Core + Qt6::Widgets + ) + +target_include_directories(pglabtests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +set_target_properties(pglabtests PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + ) + +add_test(NAME pglabtests COMMAND pglabtests) diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..4283df6 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,13 @@ +{ + "name": "pglab", + "version-string": "0.1.0", + "dependencies": [ + "boost-container", + "boost-timer", + "boost-chrono", + "boost-assert", + "botan", + "libpq", + "gtest" + ] +} From f0252a1f4993c1953e3a116558e94ebd226b7ffa Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 07:05:03 +0200 Subject: [PATCH 2/9] Remove old qmake project files. --- common.pri | 15 -- core/core.pro | 43 ----- local.pri.sample | 17 -- pglab/pglab.pro | 229 -------------------------- pglabAll.pro | 18 -- pglablib/pglablib.pro | 222 ------------------------- pgsql/pgsql.pro | 60 ------- tests/PgsqlTests/PgsqlTests.pro | 46 ------ tests/PgsqlTests/gtest_dependency.pri | 29 ---- tests/mygtestutils/mygtestutils.pro | 32 ---- tests/pglabtests/pglabtests.pro | 86 ---------- tests/tests.pro | 5 - 12 files changed, 802 deletions(-) delete mode 100644 common.pri delete mode 100644 core/core.pro delete mode 100644 local.pri.sample delete mode 100644 pglab/pglab.pro delete mode 100644 pglabAll.pro delete mode 100644 pglablib/pglablib.pro delete mode 100644 pgsql/pgsql.pro delete mode 100644 tests/PgsqlTests/PgsqlTests.pro delete mode 100644 tests/PgsqlTests/gtest_dependency.pri delete mode 100644 tests/mygtestutils/mygtestutils.pro delete mode 100644 tests/pglabtests/pglabtests.pro delete mode 100644 tests/tests.pro diff --git a/common.pri b/common.pri deleted file mode 100644 index dcff145..0000000 --- a/common.pri +++ /dev/null @@ -1,15 +0,0 @@ -! include( ./local.pri ) { -error( "Use local.pri.sample to create your own local.pri" ) -} - -LIBS += -lUser32 -lws2_32 -llibpq - -CONFIG += c++20 - -# The following define makes your compiler emit warnings if you use -# any feature of Qt which as been marked as deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS -DEFINES += BOOST_ENABLE_ASSERT_HANDLER -DEFINES += WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0501 diff --git a/core/core.pro b/core/core.pro deleted file mode 100644 index 0b424f6..0000000 --- a/core/core.pro +++ /dev/null @@ -1,43 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2017-02-26T10:51:14 -# -#------------------------------------------------- - -QT -= gui - -TARGET = core -TEMPLATE = lib -CONFIG += staticlib - -! include( ../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} - -# You can also make your code fail to compile if you use deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += my_boost_assert_handler.cpp \ - SqlLexer.cpp \ - CsvWriter.cpp \ - BackupFormatModel.cpp \ - ExplainTreeModelItem.cpp \ - jsoncpp.cpp - -HEADERS += \ - SqlLexer.h \ - ScopeGuard.h \ - CsvWriter.h \ - BackupFormatModel.h \ - Expected.h \ - ExplainTreeModelItem.h \ - json/json.h \ - std_utils.h \ - IntegerRange.h - -unix { - target.path = /usr/lib - INSTALLS += target -} diff --git a/local.pri.sample b/local.pri.sample deleted file mode 100644 index 44b8f1e..0000000 --- a/local.pri.sample +++ /dev/null @@ -1,17 +0,0 @@ -# Need paths here to find following library headers -# - botan -# - boost -# - libpq -# ??? -INCLUDEPATH += c:/prog/include -INCLUDEPATH += c:/prog/include/pgsql -INCLUDEPATH += c:/VSproj/boost/include/boost-1_68 - -# Need paths here to find library files for -# - botan -# - boost -# - libpq -# -LIBS += -L\VSproj\boost\lib -L/PROG/LIB - -GOOGLETEST_DIR = c:/prog/googletest diff --git a/pglab/pglab.pro b/pglab/pglab.pro deleted file mode 100644 index 93a9186..0000000 --- a/pglab/pglab.pro +++ /dev/null @@ -1,229 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2016-11-11T09:17:13 -# -#------------------------------------------------- - -QT += core gui concurrent - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets sql - - -TARGET = pglab -TEMPLATE = app - -! include( ../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} - -DEFINES += _WIN32_WINNT=0x0501 - -win32:RC_ICONS += pglab.ico - -SOURCES += main.cpp\ - About.cpp \ - catalog/delegates/IconColumnDelegate.cpp \ - catalog/models/BaseTableModel.cpp \ - catalog/models/ColumnTableModel.cpp \ - catalog/models/ConstraintModel.cpp \ - catalog/models/DatabasesTableModel.cpp \ - catalog/models/DependantsTableModel.cpp \ - catalog/models/IndexModel.cpp \ - catalog/models/ProcTableModel.cpp \ - catalog/models/RolesTableModel.cpp \ - catalog/models/SequenceModel.cpp \ - catalog/models/TriggerTableModel.cpp \ - catalog/tables/TablesTableModel.cpp \ - catalog/widgets/CatalogConstraintPage.cpp \ - catalog/widgets/CatalogFunctionsPage.cpp \ - catalog/widgets/CatalogIndexPage.cpp \ - catalog/widgets/CatalogInspector.cpp \ - catalog/widgets/CatalogNamespacePage.cpp \ - catalog/widgets/CatalogPageBase.cpp \ - catalog/widgets/CatalogSequencesPage.cpp \ - catalog/widgets/CatalogTablesPage.cpp \ - catalog/widgets/CatalogTypesPage.cpp \ - catalog/widgets/ColumnPage.cpp \ - catalog/widgets/DependantsPage.cpp \ - catalog/widgets/TriggerPage.cpp \ - codeeditor/CodeEditor.cpp \ - codeeditor/EditorGutter.cpp \ - codeeditor/GutterPainter.cpp \ - crud/CrudModel.cpp \ - crud/CrudTab.cpp \ - querytool/QueryExplainModel.cpp \ - querytool/QueryParamListController.cpp \ - querytool/QueryResultModel.cpp \ - querytool/QueryTool.cpp \ - serverinspector/DatabasesPage.cpp \ - serverinspector/RolesPage.cpp \ - serverinspector/ServerInspector.cpp \ - util/Colors.cpp \ - util/PgLabItemDelegate.cpp \ - util/PgLabTableView.cpp \ - util/SqlSyntaxHighlighter.cpp \ - ConnectionConfigurationWidget.cpp \ - ConnectionController.cpp \ - ManagedPage.cpp \ - PgDumpOutputHighlighter.cpp \ - CreateDatabaseDialog.cpp \ - ConnectionManagerWindow.cpp \ - ConnectionListModel.cpp \ - SslModeModel.cpp \ - stopwatch.cpp \ - TuplesResultWidget.cpp \ - BackupDialog.cpp \ - MasterController.cpp \ - ParamTypeDelegate.cpp \ - OpenDatabase.cpp \ - ProcessStdioWidget.cpp \ - ResultTableModelUtil.cpp \ - NamespaceItemModel.cpp \ - PropertyProxyModel.cpp \ - CodeGenerator.cpp \ - UserConfiguration.cpp \ - widgets/SqlCodePreview.cpp \ - CustomFilterSortModel.cpp \ - PropertiesPage.cpp \ - PasswordPromptDialog.cpp \ - EditTableWidget.cpp \ - EditColumnTableModel.cpp \ - DatabaseWindow.cpp \ - widgets/SingleRecordModel.cpp \ - widgets/SingleRecordWidget.cpp - -HEADERS += \ - About.h \ - Config.h \ - catalog/delegates/IconColumnDelegate.h \ - catalog/models/BaseTableModel.h \ - catalog/models/ColumnTableModel.h \ - catalog/models/ConstraintModel.h \ - catalog/models/DatabasesTableModel.h \ - catalog/models/DependantsTableModel.h \ - catalog/models/IndexModel.h \ - catalog/models/ProcTableModel.h \ - catalog/models/RolesTableModel.h \ - catalog/models/SequenceModel.h \ - catalog/models/TriggerTableModel.h \ - catalog/tables/TablesTableModel.h \ - catalog/widgets/CatalogConstraintPage.h \ - catalog/widgets/CatalogFunctionsPage.h \ - catalog/widgets/CatalogIndexPage.h \ - catalog/widgets/CatalogInspector.h \ - catalog/widgets/CatalogNamespacePage.h \ - catalog/widgets/CatalogPageBase.h \ - catalog/widgets/CatalogSequencesPage.h \ - catalog/widgets/CatalogTablesPage.h \ - catalog/widgets/CatalogTypesPage.h \ - catalog/widgets/ColumnPage.h \ - catalog/widgets/DependantsPage.h \ - catalog/widgets/TriggerPage.h \ - codeeditor/CodeEditor.h \ - codeeditor/EditorGutter.h \ - codeeditor/GutterPainter.h \ - crud/CrudModel.h \ - crud/CrudTab.h \ - querytool/QueryExplainModel.h \ - querytool/QueryParamListController.h \ - querytool/QueryResultModel.h \ - querytool/QueryTool.h \ - serverinspector/DatabasesPage.h \ - serverinspector/RolesPage.h \ - serverinspector/ServerInspector.h \ - util/Colors.h \ - util/PgLabItemDelegate.h \ - util/PgLabTableView.h \ - util/PgLabTableViewHelper.h \ - util/SqlSyntaxHighlighter.h \ - ConnectionConfigurationWidget.h \ - ConnectionController.h \ - IDatabaseWindow.h \ - ManagedPage.h \ - PgDumpOutputHighlighter.h \ - CreateDatabaseDialog.h \ - ConnectionManagerWindow.h \ - ConnectionListModel.h \ - SslModeModel.h \ - stopwatch.h \ - TuplesResultWidget.h \ - BackupDialog.h \ - MasterController.h \ - ParamTypeDelegate.h \ - OpenDatabase.h \ - ProcessStdioWidget.h \ - ResultTableModelUtil.h \ - NamespaceItemModel.h \ - AbstractCommand.h \ - PropertyProxyModel.h \ - CustomDataRole.h \ - CodeGenerator.h \ - UserConfiguration.h \ - widgets/SqlCodePreview.h \ - CustomFilterSortModel.h \ - PropertiesPage.h \ - PasswordPromptDialog.h \ - EditTableWidget.h \ - EditColumnTableModel.h \ - DatabaseWindow.h \ - NamespaceFilter.h \ - widgets/SingleRecordModel.h \ - widgets/SingleRecordWidget.h - -FORMS += \ - ConnectionManagerWindow.ui \ - CreateDatabaseDialog.ui \ - DatabaseWindow.ui \ - TuplesResultWidget.ui \ - querytool/QueryTab.ui \ - ProcessStdioWidget.ui \ - crud/CrudTab.ui \ - CodeGenerator.ui \ - widgets/SingleRecordWidget.ui - -RESOURCES += \ - resources.qrc - -#QMAKE_LFLAGS_WINDOWS = /SUBSYSTEM:WINDOWS,5.01 - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../core/release/ -lcore -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../core/debug/ -lcore - -INCLUDEPATH += $$PWD/../core -DEPENDPATH += $$PWD/../core - -win32:CONFIG(debug, debug|release): LIBS += -lbotand -else:win32:CONFIG(release, debug|release): LIBS += -lbotan - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/libcore.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/libcore.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/core.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/core.lib - - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../pgsql/release/ -lpgsql -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../pgsql/debug/ -lpgsql -else:unix:!macx: LIBS += -L$$OUT_PWD/../pgsql/ -lpgsql - -INCLUDEPATH += $$PWD/../pgsql -DEPENDPATH += $$PWD/../pgsql - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/release/libpgsql.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/debug/libpgsql.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/release/pgsql.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/debug/pgsql.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../pgsql/libpgsql.a - - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../pglablib/release/ -lpglablib -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../pglablib/debug/ -lpglablib -else:unix:!macx: LIBS += -L$$OUT_PWD/../pglablib/ -lpglablib - -INCLUDEPATH += $$PWD/../pglablib -DEPENDPATH += $$PWD/../pglablib - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pglablib/release/libpglablib.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pglablib/debug/libpglablib.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pglablib/release/pglablib.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pglablib/debug/pglablib.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../pglablib/libpglablib.a diff --git a/pglabAll.pro b/pglabAll.pro deleted file mode 100644 index 8631a5e..0000000 --- a/pglabAll.pro +++ /dev/null @@ -1,18 +0,0 @@ -TEMPLATE = subdirs - -DEFINES += BOOST_ENABLE_ASSERT_HANDLER - -SUBDIRS += core \ - pgsql \ - pglablib \ - pglab - -pglab.depends = core pgsql pglablib -pgsql.depends = core -pglablib.depends = core pgsql -tests.depends = core pgsql pglablib - - -CONFIG(debug, debug|release) { -SUBDIRS += tests -} diff --git a/pglablib/pglablib.pro b/pglablib/pglablib.pro deleted file mode 100644 index c41f973..0000000 --- a/pglablib/pglablib.pro +++ /dev/null @@ -1,222 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2017-12-13T17:36:43 -# -#------------------------------------------------- - -QT += widgets core - -TARGET = pglablib -TEMPLATE = lib -CONFIG += staticlib - -! include( ../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} - -# You can also make your code fail to compile if you use deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - Pglablib.cpp \ - ASyncDBConnection.cpp \ - ConnectionConfig.cpp \ - TestConnection.cpp \ - WaitHandleList.cpp \ - catalog/PgType.cpp \ - catalog/PgTypeContainer.cpp \ - catalog/PgNamespace.cpp \ - catalog/PgClass.cpp \ - catalog/PgDatabase.cpp \ - catalog/PgDatabaseContainer.cpp \ - catalog/PgAuthId.cpp \ - catalog/PgAuthIdContainer.cpp \ - catalog/PgClassContainer.cpp \ - catalog/PgDatabaseCatalog.cpp \ - catalog/PgNamespaceContainer.cpp \ - catalog/PgAttribute.cpp \ - catalog/PgContainer.cpp \ - catalog/PgAttributeContainer.cpp \ - catalog/PgIndex.cpp \ - catalog/PgIndexContainer.cpp \ - catalog/PgConstraint.cpp \ - catalog/PgConstraintContainer.cpp \ - ParamListJson.cpp \ - ParamListModel.cpp \ - sqlite/SQLiteConnection.cpp \ - sqlite/sqlite3.c \ - ui/catalog/tables/TableNode.cpp \ - ui/catalog/tables/TableSize.cpp \ - ui/catalog/tables/TableTreeBuilder.cpp \ - util.cpp \ - SqlFormattingUtils.cpp \ - catalog/PgKeywordList.cpp \ - QueryGenerator.cpp \ - catalog/PgObject.cpp \ - catalog/PgTablespace.cpp \ - catalog/PgTablespaceContainer.cpp \ - codebuilder/LanguageConfig.cpp \ - codebuilder/CodeBuilder.cpp \ - codebuilder/NameManglingRules.cpp \ - codebuilder/DefaultConfigs.cpp \ - codebuilder/TypeMappings.cpp \ - codebuilder/IndentationConfig.cpp \ - codebuilder/StructureTemplate.cpp \ - FormatToStream.cpp \ - codebuilder/StringLiteralRules.cpp \ - codebuilder/StringEscapeRule.cpp \ - catalog/PgTrigger.cpp \ - catalog/PgTriggerContainer.cpp \ - catalog/PgProc.cpp \ - catalog/PgProcContainer.cpp \ - catalog/PgDatabaseObject.cpp \ - catalog/PgServerObject.cpp \ - catalog/PgNamespaceObject.cpp \ - catalog/PgCollation.cpp \ - catalog/PgCollationContainer.cpp \ - catalog/PgInherits.cpp \ - catalog/PgInheritsContainer.cpp \ - SelectionEditorFactory.cpp \ - catalog/PgAm.cpp \ - model/TypeModelFactory.cpp \ - model/TypeSelectionItemModel.cpp \ - catalog/PgAmContainer.cpp \ - model/CollationModel.cpp \ - model/CollationModelFactory.cpp \ - catalog/PgLanguageContainer.cpp \ - catalog/PgLanguage.cpp \ - catalog/PgAcl.cpp \ - catalog/PgSequence.cpp \ - catalog/PgSequenceContainer.cpp \ - utils/HumanReadableBytes.cpp \ - utils/KeyStrengthener.cpp \ - utils/PasswordManager.cpp \ - utils/PostgresqlUrlParser.cpp - -HEADERS += \ - Pglablib.h \ - ASyncDBConnection.h \ - ConnectionConfig.h \ - TestConnection.h \ - WaitHandleList.h \ - Win32Event.h \ - catalog/PgType.h \ - catalog/PgTypeContainer.h \ - catalog/PgNamespace.h \ - catalog/PgClass.h \ - catalog/PgDatabase.h \ - catalog/PgDatabaseContainer.h \ - catalog/PgContainer.h \ - catalog/PgAuthId.h \ - catalog/PgAuthIdContainer.h \ - catalog/PgClassContainer.h \ - catalog/PgDatabaseCatalog.h \ - catalog/PgNamespaceContainer.h \ - catalog/PgAttribute.h \ - catalog/PgAttributeContainer.h \ - catalog/PgIndex.h \ - catalog/PgIndexContainer.h \ - catalog/PgConstraint.h \ - catalog/PgConstraintContainer.h \ - ParamListJson.h \ - ParamListModel.h \ - sqlite/SQLiteConnection.h \ - sqlite/sqlite3.h \ - sqlite/sqlite3ext.h \ - ui/catalog/tables/TableNode.h \ - ui/catalog/tables/TableSize.h \ - ui/catalog/tables/TableTreeBuilder.h \ - util.h \ - SqlFormattingUtils.h \ - catalog/PgCatalogTypes.h \ - catalog/PgKeywordList.h \ - QueryGenerator.h \ - catalog/PgObject.h \ - catalog/PgTablespace.h \ - catalog/PgTablespaceContainer.h \ - codebuilder/LanguageConfig.h \ - codebuilder/CodeBuilder.h \ - codebuilder/NameManglingRules.h \ - codebuilder/DefaultConfigs.h \ - codebuilder/TypeMappings.h \ - codebuilder/IndentationConfig.h \ - codebuilder/StructureTemplate.h \ - FormatToStream.h \ - codebuilder/ResultLoopTemplate.h \ - codebuilder/StringEscapeRule.h \ - codebuilder/StringLiteralRules.h \ - catalog/PgTrigger.h \ - catalog/PgTriggerContainer.h \ - catalog/PgProc.h \ - catalog/PgProcContainer.h \ - catalog/PgDatabaseObject.h \ - catalog/PgServerObject.h \ - catalog/PgNamespaceObject.h \ - catalog/PgCollation.h \ - catalog/PgCollationContainer.h \ - AbstractModelFactory.h \ - AbstractEditorFactory.h \ - catalog/PgInherits.h \ - catalog/PgInheritsContainer.h \ - SelectionEditorFactory.h \ - model/TypeSelectionItemModel.h \ - model/TypeModelFactory.h \ - catalog/PgAm.h \ - catalog/PgAmContainer.h \ - model/CollationModel.h \ - model/CollationModelFactory.h \ - catalog/PgLanguageContainer.h \ - catalog/PgLanguage.h \ - catalog/PgAcl.h \ - catalog/PgSequence.h \ - catalog/PgSequenceContainer.h \ - utils/HumanReadableBytes.h \ - utils/KeyStrengthener.h \ - utils/PasswordManager.h \ - utils/PostgresqlUrlParser.h - -unix { - target.path = /usr/lib - INSTALLS += target -} - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../core/release/ -lcore -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../core/debug/ -lcore -else:unix:!macx: LIBS += -L$$OUT_PWD/../core/ -lcore - -INCLUDEPATH += $$PWD/../core -DEPENDPATH += $$PWD/../core - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/libcore.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/libcore.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/core.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/core.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../core/libcore.a - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../pgsql/release/ -lpgsql -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../pgsql/debug/ -lpgsql -else:unix:!macx: LIBS += -L$$OUT_PWD/../pgsql/ -lpgsql - -INCLUDEPATH += $$PWD/../pgsql -DEPENDPATH += $$PWD/../pgsql - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/release/libpgsql.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/debug/libpgsql.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/release/pgsql.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../pgsql/debug/pgsql.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../pgsql/libpgsql.a - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../core/release/ -lcore -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../core/debug/ -lcore -else:unix: LIBS += -L$$OUT_PWD/../core/ -lcore - -INCLUDEPATH += $$PWD/../core -DEPENDPATH += $$PWD/../core - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/libcore.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/libcore.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/core.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/core.lib -else:unix: PRE_TARGETDEPS += $$OUT_PWD/../core/libcore.a diff --git a/pgsql/pgsql.pro b/pgsql/pgsql.pro deleted file mode 100644 index 02c21a5..0000000 --- a/pgsql/pgsql.pro +++ /dev/null @@ -1,60 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2016-11-11T09:17:13 -# -#------------------------------------------------- - -CONFIG += staticlib -QT += core - -greaterThan(QT_MAJOR_VERSION, 4): QT += widgets - -TARGET = pgsql -TEMPLATE = lib - -! include( ../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} - -LIBS += -lws2_32 -llibpq - -SOURCES += Pgsql_Connection.cpp \ - Pgsql_Params.cpp \ - Pgsql_PgException.cpp \ - Pgsql_Result.cpp \ - Pgsql_Row.cpp \ - Pgsql_Value.cpp \ - ArrayParser.cpp \ - Pgsql_oids.cpp \ - Pgsql_Transaction.cpp \ - Pgsql_ErrorDetails.cpp \ - Pgsql_Canceller.cpp - -HEADERS += Pgsql_Connection.h \ - Pgsql_IResult.h \ - Pgsql_Params.h \ - Pgsql_PgException.h \ - Pgsql_Result.h \ - Pgsql_ResultConstIterator.h \ - Pgsql_Row.h \ - Pgsql_Value.h \ - Pgsql_declare.h \ - Pgsql_Col.h \ - ArrayParser.h \ - Pgsql_oids.h \ - SqlGenerator.h \ - Pgsql_Transaction.h \ - Pgsql_ErrorDetails.h \ - Pgsql_Canceller.h - - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../core/release/ -lcore -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../core/debug/ -lcore - -INCLUDEPATH += $$PWD/../core -DEPENDPATH += $$PWD/../core - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/libcore.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/libcore.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/release/core.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../core/debug/core.lib diff --git a/tests/PgsqlTests/PgsqlTests.pro b/tests/PgsqlTests/PgsqlTests.pro deleted file mode 100644 index cb1d574..0000000 --- a/tests/PgsqlTests/PgsqlTests.pro +++ /dev/null @@ -1,46 +0,0 @@ -! include( ../../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} -include(gtest_dependency.pri) - -TEMPLATE = app -CONFIG += console -CONFIG -= app_bundle -CONFIG += thread -CONFIG += qt - -QT += core - -HEADERS += - -SOURCES += main.cpp \ - tst_Value.cpp \ - tst_ArrayParser.cpp \ - tst_Pgsql_oids.cpp - - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../../pgsql/release/ -lpgsql -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../../pgsql/debug/ -lpgsql -else:unix: LIBS += -L$$OUT_PWD/../../pgsql/ -lpgsql - -INCLUDEPATH += $$PWD/../../pgsql -DEPENDPATH += $$PWD/../../pgsql - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/release/libpgsql.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/debug/libpgsql.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/release/pgsql.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/debug/pgsql.lib -else:unix: PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/libpgsql.a - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../mygtestutils/release/ -lmygtestutils -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../mygtestutils/debug/ -lmygtestutils -else:unix:!macx: LIBS += -L$$OUT_PWD/../mygtestutils/ -lmygtestutils - -INCLUDEPATH += $$PWD/../mygtestutils -DEPENDPATH += $$PWD/../mygtestutils - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/release/libmygtestutils.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/debug/libmygtestutils.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/release/mygtestutils.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/debug/mygtestutils.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/libmygtestutils.a diff --git a/tests/PgsqlTests/gtest_dependency.pri b/tests/PgsqlTests/gtest_dependency.pri deleted file mode 100644 index d01d7f0..0000000 --- a/tests/PgsqlTests/gtest_dependency.pri +++ /dev/null @@ -1,29 +0,0 @@ -isEmpty(GOOGLETEST_DIR):GOOGLETEST_DIR=$$(GOOGLETEST_DIR) - -isEmpty(GOOGLETEST_DIR) { - warning("Using googletest src dir specified at Qt Creator wizard") - message("set GOOGLETEST_DIR as environment variable or qmake variable to get rid of this message") - GOOGLETEST_DIR = C:/Prog/googletest -} - -!isEmpty(GOOGLETEST_DIR): { - GTEST_SRCDIR = $$GOOGLETEST_DIR/googletest - GMOCK_SRCDIR = $$GOOGLETEST_DIR/googlemock -} - -requires(exists($$GTEST_SRCDIR):exists($$GMOCK_SRCDIR)) - -!exists($$GOOGLETEST_DIR):message("No googletest src dir found - set GOOGLETEST_DIR to enable.") - -DEFINES += \ - GTEST_LANG_CXX11 - -INCLUDEPATH *= \ - $$GTEST_SRCDIR \ - $$GTEST_SRCDIR/include \ - $$GMOCK_SRCDIR \ - $$GMOCK_SRCDIR/include - -SOURCES += \ - $$GTEST_SRCDIR/src/gtest-all.cc \ - $$GMOCK_SRCDIR/src/gmock-all.cc diff --git a/tests/mygtestutils/mygtestutils.pro b/tests/mygtestutils/mygtestutils.pro deleted file mode 100644 index a56a359..0000000 --- a/tests/mygtestutils/mygtestutils.pro +++ /dev/null @@ -1,32 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2017-12-01T10:07:31 -# -#------------------------------------------------- - -QT -= gui - -TARGET = mygtestutils -TEMPLATE = lib -CONFIG += staticlib - -# The following define makes your compiler emit warnings if you use -# any feature of Qt which has been marked as deprecated (the exact warnings -# depend on your compiler). Please consult the documentation of the -# deprecated API in order to know how to port your code away from it. -DEFINES += QT_DEPRECATED_WARNINGS - -# You can also make your code fail to compile if you use deprecated APIs. -# In order to do so, uncomment the following line. -# You can also select to disable deprecated APIs only up to a certain version of Qt. -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 - -SOURCES += \ - PrintTo_Qt.cpp - -HEADERS += \ - PrintTo_Qt.h -unix { - target.path = /usr/lib - INSTALLS += target -} diff --git a/tests/pglabtests/pglabtests.pro b/tests/pglabtests/pglabtests.pro deleted file mode 100644 index a666430..0000000 --- a/tests/pglabtests/pglabtests.pro +++ /dev/null @@ -1,86 +0,0 @@ -! include( ../../common.pri ) { -error( "Couldn't find the common.pri file!" ) -} -include(gtest_dependency.pri) - -TEMPLATE = app -CONFIG += console -CONFIG -= app_bundle -CONFIG += thread -CONFIG += qt - -QT += core widgets - -HEADERS += - -SOURCES += main.cpp \ - TableTreeBuilderTests.cpp \ - tst_ConvertLangToSqlString.cpp \ - tst_ConvertToMultiLineCString.cpp \ - tst_ExplainJsonParser.cpp \ - tst_HumanReadableBytes.cpp \ - tst_TryParsePostgresqlUrl.cpp \ - tst_escapeConnectionStringValue.cpp \ - tst_expected.cpp \ - tst_SqlLexer.cpp \ - tst_scopeguard.cpp \ - tst_CsvWriter.cpp \ - tst_PasswordManager.cpp \ - tst_ParamJson.cpp \ - tst_CodeBuilder.cpp \ - tst_NameManglingRules.cpp \ - tst_TypeMappings.cpp \ - tst_Range.cpp - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../../core/release/ -lcore -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../../core/debug/ -lcore - -INCLUDEPATH += $$PWD/../../core -DEPENDPATH += $$PWD/../../core - -win32:CONFIG(debug, debug|release): LIBS += -lbotand -else:win32:CONFIG(release, debug|release): LIBS += -lbotan - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../core/release/libcore.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../core/debug/libcore.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../core/release/core.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../core/debug/core.lib - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../../pgsql/release/ -lpgsql -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../../pgsql/debug/ -lpgsql -else:unix:!macx: LIBS += -L$$OUT_PWD/../../pgsql/ -lpgsql - -INCLUDEPATH += $$PWD/../../pgsql -DEPENDPATH += $$PWD/../../pgsql - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/release/libpgsql.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/debug/libpgsql.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/release/pgsql.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/debug/pgsql.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../../pgsql/libpgsql.a - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../../pglablib/release/ -lpglablib -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../../pglablib/debug/ -lpglablib -else:unix:!macx: LIBS += -L$$OUT_PWD/../../pglablib/ -lpglablib - -INCLUDEPATH += $$PWD/../../pglablib -DEPENDPATH += $$PWD/../../pglablib - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pglablib/release/libpglablib.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pglablib/debug/libpglablib.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pglablib/release/pglablib.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../pglablib/debug/pglablib.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../../pglablib/libpglablib.a - -win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../mygtestutils/release/ -lmygtestutils -else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../mygtestutils/debug/ -lmygtestutils -else:unix:!macx: LIBS += -L$$OUT_PWD/../mygtestutils/ -lmygtestutils - -INCLUDEPATH += $$PWD/../mygtestutils -DEPENDPATH += $$PWD/../mygtestutils - -win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/release/libmygtestutils.a -else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/debug/libmygtestutils.a -else:win32:!win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/release/mygtestutils.lib -else:win32:!win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/debug/mygtestutils.lib -else:unix:!macx: PRE_TARGETDEPS += $$OUT_PWD/../mygtestutils/libmygtestutils.a diff --git a/tests/tests.pro b/tests/tests.pro deleted file mode 100644 index ee23c08..0000000 --- a/tests/tests.pro +++ /dev/null @@ -1,5 +0,0 @@ -TEMPLATE = subdirs - -SUBDIRS += PgsqlTests \ - pglabtests \ - mygtestutils From 7577a39dd96c65dd4c485f2cc0cb1d40a84db9be Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 07:45:03 +0200 Subject: [PATCH 3/9] Add IDE files --- .idea/.gitignore | 10 ++ .idea/editor.xml | 367 ++++++++++++++++++++++++++++++++++++++++++++ .idea/encodings.xml | 4 + .idea/modules.xml | 8 + .idea/pgLab.iml | 8 + .idea/vcs.xml | 6 + AGENTS.md | 32 ++++ 7 files changed, 435 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/editor.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/pgLab.iml create mode 100644 .idea/vcs.xml create mode 100644 AGENTS.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..a3aeb4c --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,367 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..498ae80 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/pgLab.iml b/.idea/pgLab.iml new file mode 100644 index 0000000..6102194 --- /dev/null +++ b/.idea/pgLab.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b9c08b0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md + +pgLab: Qt 6 / C++20 desktop GUI for PostgreSQL. + +## Layout (link order: core, pgsql -> pglablib -> pglab) +- `core/` – generic utilities (SQL lexer, CSV writer, `Expected`, JSON); no DB access. +- `pgsql/` – thin C++ wrapper around libpq (connection, result, params, oids). +- `pglablib/` – catalog models, code builders, query generation, connection config. +- `pglab/` – the Qt Widgets application (executable). +- `tests/` – gtest executables `PgsqlTests` and `pglabtests`, plus `mygtestutils` (Qt `PrintTo` helpers). +- `docs/` – Sphinx user docs; `releasenotes/notes` – `reno` notes. + +## Build and test +CMake + vcpkg is the active build system (`VCPKG_ROOT` must be set). Qt 6 is not provided by vcpkg; it must be installed on the system. +``` +cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake +cmake --build build +ctest --test-dir build --output-on-failure +``` +- Run one suite: `./build/tests/pglabtests/pglabtests --gtest_filter='SqlLexer*'` (same for `PgsqlTests`). +- `-DBUILD_TESTS=OFF` skips tests. `build/` is git-ignored; the first configure builds dependencies and is slow. +- Run the app headless with `QT_QPA_PLATFORM=offscreen ./build/pglab/pglab`. It needs a PostgreSQL server for anything useful. + +## Gotchas +- Tests are registered per executable, so each `ctest` entry covers a whole suite. +- A new test file must be added to the `add_executable` list in `tests//CMakeLists.txt`. Sources are listed explicitly, with no globbing. The same goes for new sources in each library's `CMakeLists.txt`. +- `core/test/` is not referenced by any CMake file (its tests are built from `tests/pglabtests`). Don't expect changes there to be compiled. +- `AUTOMOC`/`AUTOUIC`/`AUTORCC` are on globally, so `.ui` and `.qrc` files only need to be listed as sources. +- `BOOST_ENABLE_ASSERT_HANDLER` is defined; the handler lives in `core/my_boost_assert_handler.cpp`. +- Commit messages are plain imperative sentences. User-visible changes need a reno note (`releasenotes/notes`). +- Docs build: `pip install -r docs/requirements.txt && sphinx-build -b html docs userdocs`. +- Windows (VS 2022) is also a supported platform. Avoid Linux-only code, and don't break `WIN32_LEAN_AND_MEAN`/`NOMINMAX` assumptions. From 21e8616eadc2de856184f93dd2bc72b24a4f57b6 Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 07:47:46 +0200 Subject: [PATCH 4/9] Add reno skill --- .junie/skills/reno-release-notes/SKILL.md | 110 ++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .junie/skills/reno-release-notes/SKILL.md diff --git a/.junie/skills/reno-release-notes/SKILL.md b/.junie/skills/reno-release-notes/SKILL.md new file mode 100644 index 0000000..2a1779f --- /dev/null +++ b/.junie/skills/reno-release-notes/SKILL.md @@ -0,0 +1,110 @@ +--- +name: reno-release-notes +description: Create, review and lint pgLab release notes with reno (releasenotes/notes YAML fragments). Use whenever the user mentions reno, release notes, releasenotes/notes, "reno new", "reno lint", or asks to add or check a note for a user-visible change in pgLab, even if reno is not named explicitly. +--- + +# reno release notes in pgLab + +pgLab uses [reno](https://docs.openstack.org/reno/latest/) for release notes. +Each user-visible change adds its own small YAML file in `releasenotes/notes/`, +committed together with the change. reno derives the release from git tags, so +never move or rename notes to change releases. Content is reStructuredText, not +Markdown. + +The notes are rendered in the Sphinx user docs through the +`.. release-notes::` directive in `docs/releasenotes.rst` (docs build: +`pip install -r docs/requirements.txt && sphinx-build -b html docs userdocs`). +The About dialog links to the generated `releasenotes.html`. + +## When to write a note + +Write one for: new features, behaviour changes users will notice (UI, menus, +shortcuts, query/code generation output, connection handling), bug fixes, +known issues, and upgrade impact (e.g. changed stored settings or connection +config format). + +Skip: pure refactors, build-system/CI changes, test-only changes, internal +code cleanup, docs-only edits. + +## Sections configured for this project + +Defined in `releasenotes/config.yaml` (render order). Only these keys are valid; +others (`deprecations`, `critical`) are NOT configured here and will not render. + +| Key | Rendered title | Use for | +|------------|------------------|--------------------------------------------| +| `prelude` | (implicit) | Release-wide summary, single string | +| `features` | New Features | New functionality | +| `other` | Changes | Changes to existing behaviour, minor items | +| `issues` | Known Issues | Known problems / limitations | +| `upgrade` | Upgrade Notes | What users must do or know when upgrading | +| `security` | Security Issues | Security fixes | +| `fixes` | Bug Fixes | Ordinary bug fixes | + +Every section except `prelude` is a list of strings. Encoding is utf8. + +## Creating a note + +From the repo root (`pip install reno` if missing): + +```bash +reno new short-descriptive-slug +``` + +This creates `releasenotes/notes/short-descriptive-slug-<16 hex>.yaml`. Keep the +random suffix. Existing examples: `f5-bindings-…`, +`connection-details-in-window-title-…`, `dollar-quoted-string-…`. +Delete all unused template sections, keep only what you need. + +```yaml +--- +features: + - | + The query tool now executes the current statement with + :kbd:`F5`. +fixes: + - | + Fixed reconnect dialog that kept popping up after the connection was lost. +``` + +Short single-line items may be plain (`- text`), longer ones use `- |`. +Wrap at roughly 80 columns. + +## Writing style + +- Write for pgLab users, not patch reviewers: describe the visible effect, not + the implementation (no class or function names unless users see them). +- Past or present tense, full sentences, one bullet per distinct change. +- RST markup: inline literals with double backticks (``` ``pg_catalog`` ```), + links as `` `text `_ ``, GUI menu paths with + `:menuselection:` (e.g. `:menuselection:`Help --> About``), keys with + `:kbd:`. No Markdown, no single-backtick code. +- Mention the PostgreSQL feature or object type involved when relevant. +- For `upgrade`, state the action required. + +## Validate before committing + +```bash +reno lint +reno list # note must be tracked by git to appear for tagged history +``` + +Common failures: YAML indentation/tabs, misspelled section key, a string where +a list is required (or a list for `prelude`), an unconfigured section, bad RST. +Optionally build the docs (command above) to check rendering. + +## Commit rules + +- `git add releasenotes/notes/` and commit it in the same commit as the + change, not a follow-up. +- Commit messages are plain imperative sentences (e.g. "Fix reconnect dialog + popping up repeatedly."). +- To correct a note, edit the same file; don't add a second one. + +## Troubleshooting + +- Note missing in report: not committed, or shallow clone without tags + (`git fetch --tags --unshallow`). +- Wrong release: decided by git history/tags; check `reno list`. +- Broken rendering: almost always RST (single backticks, Markdown links, + indentation). From 35eac713fff51bde8026c843b8ba45f0819e78a8 Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 07:48:33 +0200 Subject: [PATCH 5/9] Implement ResultSourceMapping --- pglablib/CMakeLists.txt | 2 + pglablib/ResultSourceMapping.cpp | 139 +++++++++++++++ pglablib/ResultSourceMapping.h | 61 +++++++ tests/pglabtests/CMakeLists.txt | 1 + tests/pglabtests/ResultSourceMappingTest.cpp | 168 +++++++++++++++++++ 5 files changed, 371 insertions(+) create mode 100644 pglablib/ResultSourceMapping.cpp create mode 100644 pglablib/ResultSourceMapping.h create mode 100644 tests/pglabtests/ResultSourceMappingTest.cpp diff --git a/pglablib/CMakeLists.txt b/pglablib/CMakeLists.txt index a3c759d..1734d09 100644 --- a/pglablib/CMakeLists.txt +++ b/pglablib/CMakeLists.txt @@ -32,6 +32,7 @@ add_library(pglablib STATIC SqlFormattingUtils.cpp catalog/PgKeywordList.cpp QueryGenerator.cpp + ResultSourceMapping.cpp catalog/PgObject.cpp catalog/PgTablespace.cpp catalog/PgTablespaceContainer.cpp @@ -107,6 +108,7 @@ add_library(pglablib STATIC catalog/PgCatalogTypes.h catalog/PgKeywordList.h QueryGenerator.h + ResultSourceMapping.h catalog/PgObject.h catalog/PgTablespace.h catalog/PgTablespaceContainer.h diff --git a/pglablib/ResultSourceMapping.cpp b/pglablib/ResultSourceMapping.cpp new file mode 100644 index 0000000..d597504 --- /dev/null +++ b/pglablib/ResultSourceMapping.cpp @@ -0,0 +1,139 @@ +#include "ResultSourceMapping.h" + +#include "Pgsql_Result.h" +#include "catalog/PgDatabaseCatalog.h" +#include "catalog/PgConstraintContainer.h" + +#include +#include + +namespace { + +/// Whether a non-key column of the table is selected more than once. +/// A table that appears twice in a join (through aliases) shows up like that, and libpq can not tell the instances apart. +bool hasAmbiguousColumn(const std::vector &columns, Oid tableOid, const std::vector &pkAttnums) +{ + for (const auto &col : columns) { + if (col.tableOid != tableOid || std::find(pkAttnums.begin(), pkAttnums.end(), col.attnum) != pkAttnums.end()) + continue; + auto count = std::count_if(columns.begin(), columns.end(), [&](const ResultColumnSource &other) { + return other.tableOid == tableOid && other.attnum == col.attnum; + }); + if (count > 1) + return true; + } + return false; +} + +} + +ResultSourceMapping::ResultSourceMapping(std::vector columns, const std::vector &primaryKeys) + : m_columns(std::move(columns)) +{ + for (const auto &col : m_columns) { + if (col.tableOid == InvalidOid || findTable(col.tableOid)) + continue; + + TableInfo info; + info.tableOid = col.tableOid; + + auto pk = std::find_if(primaryKeys.begin(), primaryKeys.end(), [&](const TablePrimaryKey &key) { + return key.tableOid == col.tableOid; + }); + if (pk != primaryKeys.end() && !pk->attnums.empty()) { + bool complete = true; + for (int attnum : pk->attnums) { + auto found = std::find_if(m_columns.begin(), m_columns.end(), [&](const ResultColumnSource &source) { + return source.tableOid == col.tableOid && source.attnum == attnum; + }); + if (found == m_columns.end()) { + complete = false; + break; + } + info.pkColumns.push_back(static_cast(std::distance(m_columns.begin(), found))); + } + info.editable = complete && !hasAmbiguousColumn(m_columns, col.tableOid, pk->attnums); + } + if (!info.editable) + info.pkColumns.clear(); + + m_tables.push_back(info); + } +} + +int ResultSourceMapping::columnCount() const +{ + return static_cast(m_columns.size()); +} + +Oid ResultSourceMapping::tableOid(int col) const +{ + if (col < 0 || col >= columnCount()) + throw std::range_error("column index out of range"); + return m_columns[col].tableOid; +} + +int ResultSourceMapping::attnum(int col) const +{ + if (col < 0 || col >= columnCount()) + throw std::range_error("column index out of range"); + return m_columns[col].attnum; +} + +bool ResultSourceMapping::isEditable(int col) const +{ + if (col < 0 || col >= columnCount()) + throw std::range_error("column index out of range"); + + const auto &source = m_columns[col]; + if (source.attnum <= 0) + return false; + + const TableInfo *info = findTable(source.tableOid); + return info && info->editable; +} + +std::vector ResultSourceMapping::primaryKeyColumns(Oid tableOid) const +{ + const TableInfo *info = findTable(tableOid); + return info ? info->pkColumns : std::vector{}; +} + +const ResultSourceMapping::TableInfo *ResultSourceMapping::findTable(Oid tableOid) const +{ + auto found = std::find_if(m_tables.begin(), m_tables.end(), [tableOid](const TableInfo &info) { + return info.tableOid == tableOid; + }); + return found == m_tables.end() ? nullptr : &*found; +} + +ResultSourceMapping createResultSourceMapping(const Pgsql::Result &result, const PgDatabaseCatalog &catalog) +{ + std::vector columns; + std::vector primaryKeys; + + for (int col = 0; col < result.cols(); ++col) { + ResultColumnSource source; + source.tableOid = result.ftable(col); + source.attnum = result.ftableCol(col); + columns.push_back(source); + + if (source.tableOid == InvalidOid) + continue; + + bool known = std::any_of(primaryKeys.begin(), primaryKeys.end(), [&](const TablePrimaryKey &key) { + return key.tableOid == source.tableOid; + }); + if (known) + continue; + + TablePrimaryKey pk; + pk.tableOid = source.tableOid; + auto constraint = catalog.constraints()->getPrimaryForRelation(source.tableOid); + if (constraint) + pk.attnums.assign(constraint->key.begin(), constraint->key.end()); + primaryKeys.push_back(pk); + } + + return ResultSourceMapping(std::move(columns), primaryKeys); +} diff --git a/pglablib/ResultSourceMapping.h b/pglablib/ResultSourceMapping.h new file mode 100644 index 0000000..b12f162 --- /dev/null +++ b/pglablib/ResultSourceMapping.h @@ -0,0 +1,61 @@ +#ifndef RESULTSOURCEMAPPING_H +#define RESULTSOURCEMAPPING_H + +#include + +#include + +class PgDatabaseCatalog; + +namespace Pgsql { + class Result; +} + +/// The table column a result column is read from, as reported by libpq (PQftable/PQftablecol). +/// tableOid is InvalidOid and attnum is 0 for expression columns. +struct ResultColumnSource { + Oid tableOid = InvalidOid; + int attnum = 0; +}; + +/// The primary key of a table, attnums in key order. +struct TablePrimaryKey { + Oid tableOid = InvalidOid; + std::vector attnums; +}; + +/// Maps the columns of a query result to the table columns they come from and decides which +/// cells can be edited. A cell is editable when it maps to a user column of a table whose full +/// primary key is present in the result and which is not ambiguous. +class ResultSourceMapping { +public: + ResultSourceMapping(std::vector columns, const std::vector &primaryKeys); + + int columnCount() const; + /// Table the column is read from, InvalidOid for expression columns. + Oid tableOid(int col) const; + /// attnum of the column in its table, 0 for expression columns. + int attnum(int col) const; + bool isEditable(int col) const; + /// Result column indices holding the primary key of the table, in key order. + /// Empty when the table is not editable. + std::vector primaryKeyColumns(Oid tableOid) const; + +private: + struct TableInfo { + Oid tableOid = InvalidOid; + bool editable = false; + std::vector pkColumns; + }; + + std::vector m_columns; + std::vector m_tables; + + const TableInfo *findTable(Oid tableOid) const; +}; + +/// Builds the mapping for a query result, looking up the primary keys of the source tables in the catalog. +ResultSourceMapping createResultSourceMapping(const Pgsql::Result &result, const PgDatabaseCatalog &catalog); + + +#endif // RESULTSOURCEMAPPING_H diff --git a/tests/pglabtests/CMakeLists.txt b/tests/pglabtests/CMakeLists.txt index f13dc29..d9f30de 100644 --- a/tests/pglabtests/CMakeLists.txt +++ b/tests/pglabtests/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable(pglabtests tst_NameManglingRules.cpp tst_TypeMappings.cpp tst_Range.cpp + ResultSourceMappingTest.cpp ) target_link_libraries(pglabtests PRIVATE diff --git a/tests/pglabtests/ResultSourceMappingTest.cpp b/tests/pglabtests/ResultSourceMappingTest.cpp new file mode 100644 index 0000000..74565e8 --- /dev/null +++ b/tests/pglabtests/ResultSourceMappingTest.cpp @@ -0,0 +1,168 @@ +#include +#include + +#include "ResultSourceMapping.h" + + +namespace { + +constexpr Oid tableA = 1001; +constexpr Oid tableB = 1002; + +ResultColumnSource src(Oid table, int attnum) +{ + ResultColumnSource s; + s.tableOid = table; + s.attnum = attnum; + return s; +} + +ResultColumnSource expression() +{ + return ResultColumnSource{}; +} + +TablePrimaryKey pk(Oid table, std::vector attnums) +{ + TablePrimaryKey key; + key.tableOid = table; + key.attnums = std::move(attnums); + return key; +} + +} + + +TEST(ResultSourceMappingTest, single_table_with_pk_all_columns_editable) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 2), src(tableA, 3)}, {pk(tableA, {1})}); + + EXPECT_EQ(m.columnCount(), 3); + EXPECT_TRUE(m.isEditable(0)); + EXPECT_TRUE(m.isEditable(1)); + EXPECT_TRUE(m.isEditable(2)); + EXPECT_EQ(m.tableOid(1), tableA); + EXPECT_EQ(m.attnum(2), 3); + EXPECT_THAT(m.primaryKeyColumns(tableA), ::testing::ElementsAre(0)); +} + +TEST(ResultSourceMappingTest, composite_pk_maps_key_columns_in_key_order) +{ + ResultSourceMapping m({src(tableA, 3), src(tableA, 1), src(tableA, 2)}, {pk(tableA, {1, 2})}); + + EXPECT_TRUE(m.isEditable(0)); + EXPECT_THAT(m.primaryKeyColumns(tableA), ::testing::ElementsAre(1, 2)); +} + +TEST(ResultSourceMappingTest, join_with_both_pks_both_tables_editable) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 2), src(tableB, 1), src(tableB, 2)}, + {pk(tableA, {1}), pk(tableB, {1})}); + + for (int col = 0; col < m.columnCount(); ++col) { + EXPECT_TRUE(m.isEditable(col)) << "column " << col; + } + EXPECT_THAT(m.primaryKeyColumns(tableA), ::testing::ElementsAre(0)); + EXPECT_THAT(m.primaryKeyColumns(tableB), ::testing::ElementsAre(2)); +} + +TEST(ResultSourceMappingTest, join_where_only_one_table_has_pk_in_result) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 2), src(tableB, 2)}, + {pk(tableA, {1}), pk(tableB, {1})}); + + EXPECT_TRUE(m.isEditable(0)); + EXPECT_TRUE(m.isEditable(1)); + EXPECT_FALSE(m.isEditable(2)); + EXPECT_TRUE(m.primaryKeyColumns(tableB).empty()); +} + +TEST(ResultSourceMappingTest, pk_missing_from_result_is_read_only) +{ + ResultSourceMapping m({src(tableA, 2), src(tableA, 3)}, {pk(tableA, {1})}); + + EXPECT_FALSE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); + EXPECT_TRUE(m.primaryKeyColumns(tableA).empty()); +} + +TEST(ResultSourceMappingTest, composite_pk_partially_present_is_read_only) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 3)}, {pk(tableA, {1, 2})}); + + EXPECT_FALSE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); +} + +TEST(ResultSourceMappingTest, table_without_pk_is_read_only) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 2)}, {}); + + EXPECT_FALSE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); +} + +TEST(ResultSourceMappingTest, expression_column_is_read_only) +{ + ResultSourceMapping m({src(tableA, 1), expression(), src(tableA, 2)}, {pk(tableA, {1})}); + + EXPECT_TRUE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); + EXPECT_EQ(m.tableOid(1), InvalidOid); + EXPECT_EQ(m.attnum(1), 0); + EXPECT_TRUE(m.isEditable(2)); +} + +TEST(ResultSourceMappingTest, system_column_is_read_only) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, -2)}, {pk(tableA, {1})}); + + EXPECT_TRUE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); +} + +TEST(ResultSourceMappingTest, duplicate_pk_column_still_editable) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 1), src(tableA, 2)}, {pk(tableA, {1})}); + + EXPECT_TRUE(m.isEditable(0)); + EXPECT_TRUE(m.isEditable(1)); + EXPECT_TRUE(m.isEditable(2)); + EXPECT_THAT(m.primaryKeyColumns(tableA), ::testing::ElementsAre(0)); +} + +TEST(ResultSourceMappingTest, same_non_pk_column_twice_makes_table_read_only) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 2), src(tableA, 2)}, {pk(tableA, {1})}); + + EXPECT_FALSE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(1)); + EXPECT_FALSE(m.isEditable(2)); + EXPECT_TRUE(m.primaryKeyColumns(tableA).empty()); +} + +TEST(ResultSourceMappingTest, self_join_with_aliased_non_pk_columns_is_read_only) +{ + ResultSourceMapping m({src(tableA, 1), src(tableA, 1), src(tableA, 2), src(tableA, 2)}, {pk(tableA, {1})}); + + EXPECT_FALSE(m.isEditable(0)); + EXPECT_FALSE(m.isEditable(2)); +} + +TEST(ResultSourceMappingTest, out_of_range_column_throws) +{ + ResultSourceMapping m({src(tableA, 1)}, {pk(tableA, {1})}); + + EXPECT_THROW(m.isEditable(1), std::range_error); + EXPECT_THROW(m.isEditable(-1), std::range_error); + EXPECT_THROW(m.tableOid(1), std::range_error); + EXPECT_THROW(m.attnum(-1), std::range_error); +} + +TEST(ResultSourceMappingTest, empty_result_has_no_columns) +{ + ResultSourceMapping m({}, {}); + + EXPECT_EQ(m.columnCount(), 0); + EXPECT_TRUE(m.primaryKeyColumns(tableA).empty()); +} From bbe52c9db8e36fd6a90b3f88b3cf1a4c8050434a Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 10:17:38 +0200 Subject: [PATCH 6/9] Generalise CrudModel to mapping-based editing --- pglab/crud/CrudModel.cpp | 235 ++++++++++++++++++++++++++------------- pglab/crud/CrudModel.h | 30 +++-- 2 files changed, 174 insertions(+), 91 deletions(-) diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index 8367f32..8a8ce00 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -4,6 +4,7 @@ #include "catalog/PgAttribute.h" #include "catalog/PgAttributeContainer.h" #include "catalog/PgConstraintContainer.h" +#include "catalog/PgClassContainer.h" #include "SqlFormattingUtils.h" #include "Pgsql_oids.h" #include "CustomDataRole.h" @@ -39,12 +40,23 @@ void CrudModel::setConfig(std::shared_ptr db, const PgClass &table { m_database = db; m_table = table; - m_primaryKey = db->catalog()->constraints()->getPrimaryForRelation(table.oid()); + m_wholeTable = true; callLoadData = true; auto dbconfig = m_database->config(); m_dbConn.setupConnection(dbconfig); } +void CrudModel::setResult(std::shared_ptr db, std::shared_ptr result) +{ + m_database = db; + m_table.reset(); + m_wholeTable = false; + callLoadData = false; + auto dbconfig = m_database->config(); + m_dbConn.setupConnection(dbconfig); + loadIntoModel(result); +} + QVariant CrudModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section == 0) @@ -232,9 +244,11 @@ void CrudModel::loadIntoModel(std::shared_ptr data) m_pendingRowList.clear(); m_roData = data; initializeColumnList(); + m_mapping.emplace(createResultSourceMapping(*m_roData, *m_database->catalog())); lastRowKey = data->rows() - 1; initRowMapping(); - appendNewRowInternal(); + if (m_wholeTable) + appendNewRowInternal(); endResetModel(); } @@ -277,7 +291,7 @@ Qt::ItemFlags CrudModel::flags(const QModelIndex &index) const if (index.column() < PreColumnCount) return flags; - if (m_primaryKey && !columnIsReadOnly(index.column())) + if (!columnIsReadOnly(index.column())) flags |= Qt::ItemIsEditable; return flags; @@ -312,22 +326,17 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol return false; } -std::tuple> CrudModel::saveRow(const PendingRow &pending_row) +std::tuple> CrudModel::saveRow(const PendingRow &pending_row, const RowMapping &row_mapping) { auto data = pending_row.data(); - RowMapping& rowmapping = m_rowMapping[pending_row.row()]; + if (data.empty()) + return { true, {} }; - if (!data.empty()) + if (row_mapping.isNew()) { QString buffer; Pgsql::Params params; - if (rowmapping.isNew()) - std::tie(buffer, params) = createInsertQuery(pending_row); - else - { - Pgsql::Params pkey_params = getPKeyParamsForRow(pending_row.row()); - std::tie(buffer, params) = createUpdateQuery(pkey_params, pending_row); - } + std::tie(buffer, params) = createInsertQuery(pending_row); Pgsql::Connection db_update_conn; auto dbconfig = m_database->config(); @@ -356,59 +365,120 @@ std::tuple> CrudModel::saveRow(const Pending msgBox.setText(ex.what()); msgBox.exec(); } + return { false, {} }; + } + + return saveUpdate(pending_row, row_mapping); +} + +std::tuple> CrudModel::saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping) +{ + std::set tables; + for (const auto &e : pending_row.data()) + tables.insert(m_mapping->tableOid(e.first)); + + try + { + Pgsql::Connection db_update_conn; + auto dbconfig = m_database->config(); + db_update_conn.connect(dbconfig.connectionString()); + auto tx = Pgsql::Transaction::startTransaction(db_update_conn); + for (Oid table_oid : tables) + { + QString buffer; + Pgsql::Params params; + std::tie(buffer, params) = createUpdateQuery(table_oid, pending_row, row_mapping); + auto result = tx.queryParam(buffer, params); + if (!result || result.tuplesAffected() != 1) + { + QMessageBox::warning(nullptr, tr("Update failed"), tr("The change did not update exactly one row in %1, the row may have changed or is no longer unique.").arg(tableName(table_oid))); + return { false, {} }; + } + } + tx.commit(); + + std::vector values; + values.reserve(m_roData->cols()); + for (int col = 0; col < m_roData->cols(); ++col) + { + auto &&pending = pending_row.data(); + auto found = pending.find(col); + values.push_back(found != pending.end() ? found->second : getSavedData(row_mapping, col)); + } + return { true, values }; + } + catch (const Pgsql::PgResultError &ex) + { + QMessageBox msgBox; + msgBox.setText(ex.what()); + msgBox.exec(); } return { false, {} }; } -Pgsql::Params CrudModel::getPKeyParamsForRow(int row) const +QString CrudModel::tableName(Oid table_oid) const { - Pgsql::Params params; - for (auto attnum : m_primaryKey->key) - { - const int col = attNumToCol(attnum); - Oid t = getType(col); - auto s = getSavedData(col, row); - params.add(s, t); - } - return params; + auto table = m_database->catalog()->classes()->getByKey(table_oid); + return table ? table->fullyQualifiedQuotedObjectName() : QString(); } -QString CrudModel::columnName(int col) const +std::vector CrudModel::resultColumnsOfTable(Oid table_oid) const { - return m_roData->getColName(col); + std::vector cols; + for (int col = 0; col < m_roData->cols(); ++col) + if (m_roData->ftable(col) == table_oid && m_roData->ftableCol(col) > 0) + cols.push_back(col); + return cols; } -std::tuple CrudModel::createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row) +QString CrudModel::attributeName(int col) const { - Pgsql::Params params; - auto data = pending_row.data(); - QString table_name = m_table->fullyQualifiedQuotedObjectName(); - QString buffer; - QTextStream q(&buffer); - q << "UPDATE " << table_name << " AS d\n SET "; - int param = 0; - for (auto& e : data) - { - if (param > 0) - q << ","; - q << quoteIdent(columnName(e.first)) << "=$" << ++param; - params.add(e.second, getType(e.first)); - } + if (col < static_cast(columnList.size()) && columnList[col]) + return columnList[col]->name; + return QString::fromUtf8(m_roData->getColName(col)); +} - q << "\nWHERE "; - int i = 0; - for (auto attnum : m_primaryKey->key) +Pgsql::Params CrudModel::getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const +{ + Pgsql::Params params; + for (int col : m_mapping->primaryKeyColumns(table_oid)) { - int col = attNumToCol(attnum); - if (i > 0) - q << " AND "; - q << quoteIdent(columnName(col)) << "=$" << ++param; - ++i; - } - params.addParams(pkey_params); - q << "\nRETURNING *"; - q.flush(); - return { buffer, params }; + auto s = getSavedData(row_mapping, col); + params.add(s, getType(col)); + } + return params; +} + +std::tuple CrudModel::createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping) +{ + Pgsql::Params params; + QString table_name = tableName(table_oid); + QString buffer; + QTextStream q(&buffer); + q << "UPDATE " << table_name << " AS d\n SET "; + int param = 0; + for (auto& e : pending_row.data()) + { + if (m_mapping->tableOid(e.first) != table_oid) + continue; + if (param > 0) + q << ","; + q << quoteIdent(attributeName(e.first)) << "=$" << ++param; + params.add(e.second, getType(e.first)); + } + + q << "\nWHERE "; + int i = 0; + for (int col : m_mapping->primaryKeyColumns(table_oid)) + { + if (i > 0) + q << " AND "; + q << quoteIdent(attributeName(col)) << "=$" << ++param; + ++i; + } + params.addParams(getPKeyParamsForRow(row_mapping, table_oid)); + q.flush(); + return { buffer, params }; } std::tuple CrudModel::createInsertQuery(const PendingRow &pending_row) @@ -424,12 +494,11 @@ std::tuple CrudModel::createInsertQuery(const PendingRow for (const auto& e : data) { int num = e.first; - auto&& column = columnList[num]; if (first) first = false; else q << ","; - q << quoteIdent(column.name); + q << quoteIdent(attributeName(num)); } q << ") VALUES ($1"; for (size_t p = 2; p <= data.size(); ++p) @@ -442,19 +511,6 @@ std::tuple CrudModel::createInsertQuery(const PendingRow return { buffer, params }; } -std::tuple CrudModel::createDeleteStatement(const PKeyValues &pkey_values) -{ - Pgsql::Params params; - size_t i = 0; - for (auto attnum : m_primaryKey->key) - { - const int col = attNumToCol(attnum); - params.add(pkey_values[i].c_str(), getType(col)); - ++i; - } - - return { createDeleteStatement(), params }; -} QString CrudModel::createDeleteStatement() const { @@ -465,12 +521,11 @@ QString CrudModel::createDeleteStatement() const q << "DELETE FROM " << table_name; q << "\nWHERE "; int i = 0; - for (auto attnum : m_primaryKey->key) + for (int col : m_mapping->primaryKeyColumns(m_table->oid())) { - const int col = attNumToCol(attnum); if (i > 0) q << " AND "; - q << quoteIdent(columnName(col)) << "=$" << ++i; + q << quoteIdent(attributeName(col)) << "=$" << ++i; } q.flush(); @@ -483,7 +538,13 @@ bool CrudModel::savePendingChanges() while (!m_pendingRowList.m_rows.empty()) { auto iter = m_pendingRowList.m_rows.begin(); - auto [ok, modified_row] = saveRow(iter->second); + int pendingKey = iter->first; + auto mapping_iter = std::find_if(m_rowMapping.begin(), m_rowMapping.end(), + [pendingKey](const RowMapping &rhs) -> bool { return rhs.rowKey == pendingKey; }); + if (mapping_iter == m_rowMapping.end()) + return false; + + auto [ok, modified_row] = saveRow(iter->second, *mapping_iter); if (ok) { int rowKey = iter->first; @@ -516,9 +577,13 @@ bool CrudModel::columnIsReadOnly(int column_index) const if (m_roData == nullptr) return true; - auto&& column = columnList[column_index - PreColumnCount]; - return column.getIdentity() == PgAttribute::Identity::Always - || column.getGenerated() != PgAttribute::Generated::None; + int col = column_index - PreColumnCount; + if (!m_mapping || !m_mapping->isEditable(col)) + return true; + + const auto &column = columnList[col]; + return !column || column->getIdentity() == PgAttribute::Identity::Always + || column->getGenerated() != PgAttribute::Generated::None; } void CrudModel::initializeColumnList() @@ -526,13 +591,20 @@ void CrudModel::initializeColumnList() columnList.clear(); columnList.reserve(m_roData->cols()); auto atts = m_database->catalog()->attributes(); - auto columns = atts->getColumnsForRelation(m_table->oid()); for (int col = 0; col < m_roData->cols(); ++col) { int attnum = m_roData->ftableCol(col); + if (attnum <= 0) + { + columnList.emplace_back(); + continue; + } + auto columns = atts->getColumnsForRelation(m_roData->ftable(col)); auto find_result = std::find_if(columns.begin(), columns.end(), [attnum](const PgAttribute &att) { return att.num == attnum; }); - assert(find_result != columns.end()); - columnList.push_back(*find_result); + if (find_result != columns.end()) + columnList.emplace_back(*find_result); + else + columnList.emplace_back(); } } @@ -548,6 +620,9 @@ void CrudModel::revert() void CrudModel::appendNewRow() { + if (!m_wholeTable) + return; + int row = static_cast(m_rowMapping.size()); beginInsertRows(QModelIndex(), row, row); appendNewRowInternal(); @@ -565,6 +640,8 @@ std::tuple CrudModel::removeRows(const std::set return { true, "" }; if (row_ranges.rbegin()->end() > static_cast(m_rowMapping.size())) return { false, "Range error" }; + if (!m_wholeTable || !m_table || m_mapping->primaryKeyColumns(m_table->oid()).empty()) + return { false, "Rows can only be removed from a table with a primary key" }; // When removing rows there is no direct mapping anymore between the rows in the grid // and the rows in m_roData @@ -589,7 +666,7 @@ std::tuple CrudModel::removeRows(const std::set for (int current_row = range.start(); current_row < range.end(); ++current_row) { auto&& mapping = m_rowMapping[static_cast(current_row)]; - auto params = getPKeyParamsForRow(mapping.rowKey); + auto params = getPKeyParamsForRow(mapping, m_table->oid()); if (!params.empty()) db_update_conn.queryParam(delete_statement, params); } diff --git a/pglab/crud/CrudModel.h b/pglab/crud/CrudModel.h index 5a6beb3..d05b6c0 100644 --- a/pglab/crud/CrudModel.h +++ b/pglab/crud/CrudModel.h @@ -5,8 +5,10 @@ #include "Pgsql_Connection.h" #include "IntegerRange.h" +#include "ResultSourceMapping.h" #include "catalog/PgClass.h" #include "catalog/PgConstraint.h" +#include "catalog/PgAttribute.h" #include #include #include @@ -26,6 +28,8 @@ public: ~CrudModel() override; void setConfig(std::shared_ptr db, const PgClass &table); + /// Show an arbitrary query result, cells are editable where the result maps them to a table with a primary key + void setResult(std::shared_ptr db, std::shared_ptr result); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; @@ -50,7 +54,6 @@ public slots: virtual void revert() override; private: - using PKeyValues = std::vector; const int PreColumnCount = 1; @@ -76,6 +79,7 @@ private: res += " NULLS FIRST"; else if (nulls == NullSorting::Last) res += " NULLS LAST"; + return res; } }; @@ -205,10 +209,11 @@ private: std::shared_ptr m_database; std::optional m_table; - std::optional m_primaryKey; + std::optional m_mapping; ASyncDBConnection m_dbConn; bool callLoadData = false; + bool m_wholeTable = false; ///< true when showing a whole table (CRUD), false for an arbitrary query result std::shared_ptr m_roData; @@ -218,7 +223,7 @@ private: // identity allways or generated }; - std::vector columnList; // list of columnMeta 1 to 1 with columns in m_roData. + std::vector> columnList; // attribute per column in m_roData, empty for expression columns PendingRowList m_pendingRowList; @@ -238,29 +243,30 @@ private: Oid getType(int column) const; - QString columnName(int col) const; /// Get the PKey values from when the row was last restored or retrieved. - Pgsql::Params getPKeyParamsForRow(int row) const; + Pgsql::Params getPKeyParamsForRow(const RowMapping &row_mapping, Oid table_oid) const; bool savePendingChanges(); - std::tuple createUpdateQuery(const Pgsql::Params &pkey_params, const PendingRow &pending_row); + std::tuple createUpdateQuery(Oid table_oid, const PendingRow &pending_row, const RowMapping &row_mapping); std::tuple createInsertQuery(const PendingRow &pending_row); - std::tuple createDeleteStatement(const PKeyValues &pkey_values); QString createDeleteStatement() const; - std::tuple> saveRow(const PendingRow &pending_row); + std::tuple> saveRow(const PendingRow &pending_row, const RowMapping &row_mapping); + std::tuple> saveUpdate(const PendingRow &pending_row, const RowMapping &row_mapping); void appendNewRow(); void appendNewRowInternal(); int lastRowKey = -1; int allocNewRowKey() { return ++lastRowKey; } - /// Convert an attnum from the database catalog to the corresponding column in the model - /// - /// \todo still assumes columns are in order, all being shown and no special column like oid shown. - int attNumToCol(int attnum) const { return attnum - 1; } + /// Name of the table with the given oid, quoted and schema qualified + QString tableName(Oid table_oid) const; + /// Result columns that are user columns (attnum > 0) of the given table, in result order + std::vector resultColumnsOfTable(Oid table_oid) const; + /// Catalog name of the attribute behind a result column, falls back to the result column name + QString attributeName(int col) const; void RemoveRangesOfRowsFromModel(const std::set> &row_ranges); void RemoveRangeOfRowsFromModel(IntegerRange row_range); From 5e7fb68c55b308332f92915636f692cbbe88602e Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 10:29:24 +0200 Subject: [PATCH 7/9] QueryTool now uses the same DataGrid as the CRUD tool. --- pglab/CMakeLists.txt | 2 +- pglab/crud/CrudModel.cpp | 3 + pglab/crud/CrudTab.cpp | 38 ++++----- pglab/crud/CrudTab.h | 4 +- pglab/crud/CrudTab.ui | 10 +-- pglab/crud/DataGridWidget.cpp | 34 ++++++++ pglab/crud/DataGridWidget.h | 28 ++++++ pglab/querytool/QueryResultModel.cpp | 85 ------------------- pglab/querytool/QueryResultModel.h | 31 ------- pglab/querytool/QueryTool.cpp | 4 +- pglab/querytool/QueryTool.h | 2 - pglab/tuplesresultwidget.cpp | 35 +++++--- pglab/tuplesresultwidget.h | 19 +++-- pglab/tuplesresultwidget.ui | 31 ------- ...esults-editable-grid-b265a193a39f3245.yaml | 12 +++ 15 files changed, 135 insertions(+), 203 deletions(-) create mode 100644 pglab/crud/DataGridWidget.cpp create mode 100644 pglab/crud/DataGridWidget.h delete mode 100644 pglab/querytool/QueryResultModel.cpp delete mode 100644 pglab/querytool/QueryResultModel.h create mode 100644 releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml diff --git a/pglab/CMakeLists.txt b/pglab/CMakeLists.txt index ab4dda5..fb3d3f2 100644 --- a/pglab/CMakeLists.txt +++ b/pglab/CMakeLists.txt @@ -30,9 +30,9 @@ set(PGLAB_SOURCES codeeditor/GutterPainter.cpp crud/CrudModel.cpp crud/CrudTab.cpp + crud/DataGridWidget.cpp querytool/QueryExplainModel.cpp querytool/QueryParamListController.cpp - querytool/QueryResultModel.cpp querytool/QueryTool.cpp serverinspector/DatabasesPage.cpp serverinspector/RolesPage.cpp diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index 8a8ce00..a8bcae1 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -10,6 +10,7 @@ #include "CustomDataRole.h" #include #include +#include #include #include "Pgsql_oids.h" #include "Pgsql_PgException.h" @@ -217,6 +218,8 @@ QVariant CrudModel::data(const QModelIndex &index, int role) const if (value) return StringToVariant(value->c_str(), typ); } + else if (role == Qt::ForegroundRole && !m_wholeTable && columnIsReadOnly(index.column())) + return QBrush(Qt::gray); return {}; } diff --git a/pglab/crud/CrudTab.cpp b/pglab/crud/CrudTab.cpp index a776879..88b0ac2 100644 --- a/pglab/crud/CrudTab.cpp +++ b/pglab/crud/CrudTab.cpp @@ -3,16 +3,19 @@ #include "CrudModel.h" #include "CustomDataRole.h" #include "ResultTableModelUtil.h" -#include "util/PgLabItemDelegate.h" +#include "DataGridWidget.h" #include "IntegerRange.h" #include "OpenDatabase.h" #include "catalog/PgClassContainer.h" #include "catalog/PgDatabaseCatalog.h" #include +#include #include +#include #include #include #include +#include CrudTab::CrudTab(IDatabaseWindow *context, QWidget *parent) @@ -24,25 +27,18 @@ CrudTab::CrudTab(IDatabaseWindow *context, QWidget *parent) m_db = context->openDatabase(); - SetTableViewDefault(ui->tableView); - - auto delegate = new PgLabItemDelegate(ui->tableView); - ui->tableView->setItemDelegate(delegate); - m_crudModel = new CrudModel(nullptr); - m_SortFilterProxy = new QSortFilterProxyModel(this); - m_SortFilterProxy->setSourceModel(m_crudModel); - m_SortFilterProxy->setSortRole(CustomSortRole); - ui->tableView->setModel(m_SortFilterProxy); - ui->tableView->setSortingEnabled(true); - m_SortFilterProxy->sort(0, Qt::AscendingOrder); + m_grid = new DataGridWidget(this); + ui->verticalLayout->addWidget(m_grid); + m_grid->setSourceModel(m_crudModel); + m_grid->view()->verticalHeader()->setVisible(false); - ui->tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); - ui->tableView->setSelectionBehavior(QAbstractItemView::SelectRows); - ui->tableView->addAction(ui->actionRemove_rows); + m_grid->view()->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_grid->view()->setSelectionBehavior(QAbstractItemView::SelectRows); + m_grid->view()->addAction(ui->actionRemove_rows); - auto horizontal_header = ui->tableView->horizontalHeader(); + auto horizontal_header = m_grid->view()->horizontalHeader(); horizontal_header->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(horizontal_header, &QHeaderView::customContextMenuRequested, this, &CrudTab::headerCustomContextMenu); @@ -70,7 +66,7 @@ void CrudTab::refresh() void CrudTab::on_actionRemove_rows_triggered() { std::set> row_ranges; - auto selection = m_SortFilterProxy->mapSelectionToSource(ui->tableView->selectionModel()->selection()); + auto selection = m_grid->proxy()->mapSelectionToSource(m_grid->view()->selectionModel()->selection()); for (auto& range : selection) { row_ranges.emplace(range.top(), range.height()); } @@ -110,7 +106,7 @@ void CrudTab::headerCustomContextMenu(const QPoint &pos) if (headerContextMenu) { - auto horizontal_header = ui->tableView->horizontalHeader(); + auto horizontal_header = m_grid->view()->horizontalHeader(); headerContextMenu->popup(horizontal_header->mapToGlobal(pos)); } } @@ -122,9 +118,9 @@ void CrudTab::gotoColumn(QString column) auto name = m_crudModel->headerData(i, Qt::Horizontal).toString(); if (name == column) { - auto index = ui->tableView->currentIndex(); - index = ui->tableView->model()->index(index.row(), i); - ui->tableView->setCurrentIndex(index); + auto index = m_grid->view()->currentIndex(); + index = m_grid->view()->model()->index(index.row(), i); + m_grid->view()->setCurrentIndex(index); break; } } diff --git a/pglab/crud/CrudTab.h b/pglab/crud/CrudTab.h index ef91325..a6bf0fc 100644 --- a/pglab/crud/CrudTab.h +++ b/pglab/crud/CrudTab.h @@ -1,7 +1,6 @@ #ifndef CRUDTAB_H #define CRUDTAB_H -#include #include "catalog/PgClass.h" #include "IDatabaseWindow.h" #include @@ -14,6 +13,7 @@ namespace Ui { class OpenDatabase; class CrudModel; +class DataGridWidget; class QMenu; class CrudTab : public QWidget { @@ -33,7 +33,7 @@ private: std::optional m_table; CrudModel *m_crudModel = nullptr; - QSortFilterProxyModel *m_SortFilterProxy = nullptr; + DataGridWidget *m_grid = nullptr; QMenu *headerContextMenu = nullptr; diff --git a/pglab/crud/CrudTab.ui b/pglab/crud/CrudTab.ui index 8c58abd..f3118a4 100644 --- a/pglab/crud/CrudTab.ui +++ b/pglab/crud/CrudTab.ui @@ -13,15 +13,7 @@ Form - - - - - false - - - - + Remove rows diff --git a/pglab/crud/DataGridWidget.cpp b/pglab/crud/DataGridWidget.cpp new file mode 100644 index 0000000..64f19a8 --- /dev/null +++ b/pglab/crud/DataGridWidget.cpp @@ -0,0 +1,34 @@ +#include "DataGridWidget.h" +#include "CustomDataRole.h" +#include "ResultTableModelUtil.h" +#include "util/PgLabItemDelegate.h" +#include +#include +#include + + +DataGridWidget::DataGridWidget(QWidget *parent) + : QWidget(parent) +{ + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_view = new QTableView(this); + layout->addWidget(m_view); + + SetTableViewDefault(m_view); + auto delegate = new PgLabItemDelegate(m_view); + m_view->setItemDelegate(delegate); +} + +void DataGridWidget::setSourceModel(QAbstractItemModel *model) +{ + if (!m_proxy) { + m_proxy = new QSortFilterProxyModel(this); + m_proxy->setSortRole(CustomSortRole); + } + m_proxy->setSourceModel(model); + m_view->setModel(m_proxy); + m_view->setSortingEnabled(true); + m_proxy->sort(0, Qt::AscendingOrder); +} diff --git a/pglab/crud/DataGridWidget.h b/pglab/crud/DataGridWidget.h new file mode 100644 index 0000000..11d6745 --- /dev/null +++ b/pglab/crud/DataGridWidget.h @@ -0,0 +1,28 @@ +#ifndef DATAGRIDWIDGET_H +#define DATAGRIDWIDGET_H + +#include + +class QAbstractItemModel; +class QSortFilterProxyModel; +class QTableView; + +/** Sortable and editable grid shared by the CRUD tab and the query result tab. + */ +class DataGridWidget : public QWidget { + Q_OBJECT +public: + explicit DataGridWidget(QWidget *parent = nullptr); + + /// Sets the model to show, sorting is done by a proxy that uses CustomSortRole + void setSourceModel(QAbstractItemModel *model); + + QTableView *view() const { return m_view; } + QSortFilterProxyModel *proxy() const { return m_proxy; } + +private: + QTableView *m_view = nullptr; + QSortFilterProxyModel *m_proxy = nullptr; +}; + +#endif // DATAGRIDWIDGET_H diff --git a/pglab/querytool/QueryResultModel.cpp b/pglab/querytool/QueryResultModel.cpp deleted file mode 100644 index 360a00c..0000000 --- a/pglab/querytool/QueryResultModel.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "QueryResultModel.h" -#include "ResultTableModelUtil.h" -#include "Pgsql_declare.h" -#include "Pgsql_oids.h" -#include "catalog/PgDatabaseCatalog.h" -#include "CustomDataRole.h" -#include -#include - -using namespace Pgsql; - -QueryResultModel::QueryResultModel(QObject *parent, std::shared_ptr r - , std::shared_ptr cat) - : QAbstractTableModel(parent) - , result(std::move(r)) - , catalog(cat) -{} - - -int QueryResultModel::rowCount(const QModelIndex &) const -{ - int r = result->rows(); - return r; -} - -int QueryResultModel::columnCount(const QModelIndex &) const -{ - int r = result->cols(); - return r; -} - -QVariant QueryResultModel::data(const QModelIndex &index, int role) const -{ -// static const QString null_str("null"); - QVariant v; - - const int col = index.column(); - const int rij = index.row(); - const Oid oid = result->type(col); - if (role == Qt::DisplayRole || role == Qt::EditRole) { - if (result->null(col, rij)) { - //v = nullptr; - } - else { - Value value = result->get(col, rij); - if (oid == bool_oid) { - bool b = value; - v = b; - } - else { - QString s = value; - v = s; - } - } - } - else if (role == CustomDataTypeRole) { - v = oid; - } - - - return v; -} - -QVariant QueryResultModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - QVariant r; - if (role == Qt::DisplayRole) { - if (orientation == Qt::Horizontal) { - QString s(result->getColName(section)); - s += "\n"; - s += getTypeDisplayString(*catalog, result->type(section)); - r = s; - } - else { - r = QString::number(section + 1); - } - } - Qt::ItemFlags f; - return r; -} - -Qt::ItemFlags QueryResultModel::flags(const QModelIndex &) const -{ - return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; -} diff --git a/pglab/querytool/QueryResultModel.h b/pglab/querytool/QueryResultModel.h deleted file mode 100644 index 7de2f70..0000000 --- a/pglab/querytool/QueryResultModel.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef QUERYRESULTMODEL_H -#define QUERYRESULTMODEL_H - -#include -#include "catalog/models/BaseTableModel.h" -#include "Pgsql_Connection.h" - -class PgDatabaseCatalog; - -class QueryResultModel : public QAbstractTableModel -{ - Q_OBJECT -public: - QueryResultModel(QObject *parent, std::shared_ptr r, std::shared_ptr cat); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - virtual Qt::ItemFlags flags(const QModelIndex &index) const override; - - std::shared_ptr GetPgsqlResult() const { return result; } -protected: -// virtual Oid getType(int column) const override; -// virtual QVariant getData(const QModelIndex &index) const override; -private: - std::shared_ptr result; - std::shared_ptr catalog; -}; - -#endif // QUERYRESULTMODEL_H diff --git a/pglab/querytool/QueryTool.cpp b/pglab/querytool/QueryTool.cpp index 153b19c..f808335 100644 --- a/pglab/querytool/QueryTool.cpp +++ b/pglab/querytool/QueryTool.cpp @@ -476,10 +476,8 @@ void QueryTool::query_ready(std::shared_ptr dbres, qint64 elapsed //int n_rows = dbres->getRows(); //QString rowcount_str = QString("rows: %1").arg(dbres->getRows()); - auto result_model = std::make_shared(nullptr , dbres, - m_catalog); TuplesResultWidget *trw = new TuplesResultWidget; - trw->setResult(result_model, elapsedms); + trw->setResult(m_context->openDatabase(), dbres, elapsedms); resultList.push_back(trw); ui->tabWidget->addTab(trw, "Data"); if (resultList.size() == 1) diff --git a/pglab/querytool/QueryTool.h b/pglab/querytool/QueryTool.h index 5cb98f0..1274642 100644 --- a/pglab/querytool/QueryTool.h +++ b/pglab/querytool/QueryTool.h @@ -3,7 +3,6 @@ #include "ManagedPage.h" #include "ASyncDBConnection.h" -#include "QueryResultModel.h" #include "QueryExplainModel.h" #include "stopwatch.h" #include "tuplesresultwidget.h" @@ -26,7 +25,6 @@ class QTableView; class QTabWidget; class SqlSyntaxHighlighter; class ExplainRoot; -class QueryResultModel; class QueryExplainModel; class PgTypeContainer; class OpenDatabase; diff --git a/pglab/tuplesresultwidget.cpp b/pglab/tuplesresultwidget.cpp index 1e2dd5b..21b090a 100644 --- a/pglab/tuplesresultwidget.cpp +++ b/pglab/tuplesresultwidget.cpp @@ -1,11 +1,15 @@ -#include "tuplesresultwidget.h" +#include "tuplesresultwidget.h" #include "ui_tuplesresultwidget.h" #include "ResultTableModelUtil.h" #include "util.h" +#include "crud/CrudModel.h" +#include "crud/DataGridWidget.h" +#include "OpenDatabase.h" #include #include -#include "util/PgLabItemDelegate.h" +#include +#include TuplesResultWidget::TuplesResultWidget(QWidget *parent) : @@ -15,9 +19,13 @@ TuplesResultWidget::TuplesResultWidget(QWidget *parent) : ui->setupUi(this); ui->lblRowCount->setText(QString()); - SetTableViewDefault(ui->ResultView); - auto delegate = new PgLabItemDelegate(ui->ResultView); - ui->ResultView->setItemDelegate(delegate); + m_grid = new DataGridWidget(this); + ui->verticalLayout->insertWidget(0, m_grid); + m_grid->view()->setFont(QFont("Source Sans Pro", 10)); + m_grid->view()->setAlternatingRowColors(true); + m_grid->view()->setWordWrap(false); + m_grid->view()->verticalHeader()->setDefaultSectionSize(21); + m_grid->view()->verticalHeader()->setMinimumSectionSize(16); } TuplesResultWidget::~TuplesResultWidget() @@ -25,14 +33,17 @@ TuplesResultWidget::~TuplesResultWidget() delete ui; } -void TuplesResultWidget::setResult(std::shared_ptr res, float ms) +void TuplesResultWidget::setResult(std::shared_ptr db, std::shared_ptr res, float ms) { - resultModel = res; - ui->ResultView->setModel(resultModel.get()); + m_result = res; - ui->ResultView->resizeColumnsToContents(); + m_model = new CrudModel(m_grid); + m_model->setResult(db, res); + m_grid->setSourceModel(m_model); - QString rowcount_str = QString("rows: %1").arg(resultModel->rowCount()); + m_grid->view()->resizeColumnsToContents(); + + QString rowcount_str = QString("rows: %1").arg(m_model->rowCount()); ui->lblRowCount->setText(rowcount_str); ui->lblElapsedTime->setText(msfloatToHumanReadableString(ms)); } @@ -42,8 +53,6 @@ void TuplesResultWidget::exportData(const QString &file_name) const QFile file(file_name); if (file.open(QIODevice::WriteOnly)) { QTextStream out(&file); - ::exportTable(ui->ResultView, out); + ::exportTable(m_grid->view(), out); } } - - diff --git a/pglab/tuplesresultwidget.h b/pglab/tuplesresultwidget.h index 89a55d9..19fc083 100644 --- a/pglab/tuplesresultwidget.h +++ b/pglab/tuplesresultwidget.h @@ -1,13 +1,21 @@ #ifndef TUPLESRESULTWIDGET_H #define TUPLESRESULTWIDGET_H -#include "querytool/QueryResultModel.h" #include +#include namespace Ui { class TuplesResultWidget; } +namespace Pgsql { + class Result; +} + +class OpenDatabase; +class CrudModel; +class DataGridWidget; + class TuplesResultWidget : public QWidget { Q_OBJECT @@ -16,14 +24,15 @@ public: explicit TuplesResultWidget(QWidget *parent = 0); ~TuplesResultWidget(); - void setResult(std::shared_ptr res, float ms); + void setResult(std::shared_ptr db, std::shared_ptr res, float ms); void exportData(const QString &file_name) const; - std::shared_ptr GetPgsqlResult() const { return resultModel->GetPgsqlResult(); } + std::shared_ptr GetPgsqlResult() const { return m_result; } private: Ui::TuplesResultWidget *ui; - - std::shared_ptr resultModel; + DataGridWidget *m_grid = nullptr; + CrudModel *m_model = nullptr; + std::shared_ptr m_result; }; #endif // TUPLESRESULTWIDGET_H diff --git a/pglab/tuplesresultwidget.ui b/pglab/tuplesresultwidget.ui index 677a464..b13909e 100644 --- a/pglab/tuplesresultwidget.ui +++ b/pglab/tuplesresultwidget.ui @@ -29,37 +29,6 @@ 2 - - - - - Source Sans Pro - 10 - - - - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - - - true - - - QAbstractItemView::ScrollPerPixel - - - QAbstractItemView::ScrollPerPixel - - - false - - - 21 - - - 16 - - - diff --git a/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml b/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml new file mode 100644 index 0000000..14be544 --- /dev/null +++ b/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Query results are now shown in the same grid as the table data tab. Columns + can be sorted by clicking their headers, and cells that belong to a table + with a primary key present in the result can be edited directly. Changes + are saved with an ``UPDATE`` by primary key. +other: + - | + Read-only cells in query results, such as expression columns or columns of + tables without a primary key in the result, are now shown in grey so they + can be told apart from editable cells. From 66267334c82fce58d5afb6508e3984901df8dc9e Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 10:35:39 +0200 Subject: [PATCH 8/9] We should not allow editing of system catalogs. --- pglablib/ResultSourceMapping.cpp | 26 ++++++++++++++++--- ...esults-editable-grid-b265a193a39f3245.yaml | 9 ++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/pglablib/ResultSourceMapping.cpp b/pglablib/ResultSourceMapping.cpp index d597504..8197133 100644 --- a/pglablib/ResultSourceMapping.cpp +++ b/pglablib/ResultSourceMapping.cpp @@ -1,8 +1,12 @@ #include "ResultSourceMapping.h" #include "Pgsql_Result.h" +#include "catalog/PgClass.h" #include "catalog/PgDatabaseCatalog.h" #include "catalog/PgConstraintContainer.h" +#include "catalog/PgNamespace.h" +#include "catalog/PgNamespaceContainer.h" +#include "catalog/PgClassContainer.h" #include #include @@ -107,6 +111,20 @@ const ResultSourceMapping::TableInfo *ResultSourceMapping::findTable(Oid tableOi return found == m_tables.end() ? nullptr : &*found; } +namespace { + +/// Catalog tables (pg_catalog, information_schema, ...) are views or system tables and must never be edited. +bool isSystemCatalogTable(const PgDatabaseCatalog &catalog, Oid tableOid) +{ + const PgClass *cls = catalog.classes()->getByKey(tableOid); + if (!cls) + return false; + const PgNamespace *ns = catalog.namespaces()->getByKey(cls->nsOid()); + return ns && ns->isSystemCatalog(); +} + +} + ResultSourceMapping createResultSourceMapping(const Pgsql::Result &result, const PgDatabaseCatalog &catalog) { std::vector columns; @@ -129,9 +147,11 @@ ResultSourceMapping createResultSourceMapping(const Pgsql::Result &result, const TablePrimaryKey pk; pk.tableOid = source.tableOid; - auto constraint = catalog.constraints()->getPrimaryForRelation(source.tableOid); - if (constraint) - pk.attnums.assign(constraint->key.begin(), constraint->key.end()); + if (!isSystemCatalogTable(catalog, source.tableOid)) { + auto constraint = catalog.constraints()->getPrimaryForRelation(source.tableOid); + if (constraint) + pk.attnums.assign(constraint->key.begin(), constraint->key.end()); + } primaryKeys.push_back(pk); } diff --git a/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml b/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml index 14be544..a54b9f5 100644 --- a/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml +++ b/releasenotes/notes/query-results-editable-grid-b265a193a39f3245.yaml @@ -4,9 +4,10 @@ features: Query results are now shown in the same grid as the table data tab. Columns can be sorted by clicking their headers, and cells that belong to a table with a primary key present in the result can be edited directly. Changes - are saved with an ``UPDATE`` by primary key. + are saved with an ``UPDATE`` by primary key. Tables in system catalog + schemas such as ``pg_catalog`` and ``information_schema`` are never editable. other: - | - Read-only cells in query results, such as expression columns or columns of - tables without a primary key in the result, are now shown in grey so they - can be told apart from editable cells. + Read-only cells in query results, such as expression columns, columns of + tables without a primary key in the result, or columns of system catalog + tables, are now shown in grey so they can be told apart from editable cells. From ee1a8ba48dea2ea96ada9e75bfb05d78150889c4 Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 18 Oct 2026 11:34:00 +0200 Subject: [PATCH 9/9] Add save and revert button to datagrid no implicit saves any more. --- .idea/QtSettings.xml | 6 ++ .idea/misc.xml | 7 ++ .idea/pgLab.iml | 8 +-- pglab/crud/CrudModel.cpp | 30 ++++++++- pglab/crud/CrudModel.h | 13 +++- pglab/crud/CrudTab.cpp | 7 ++ pglab/crud/DataGridWidget.cpp | 67 +++++++++++++++++++ pglab/crud/DataGridWidget.h | 14 ++++ ...-save-revert-toolbar-3f9c1a7e5b2d8604.yaml | 13 ++++ 9 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 .idea/QtSettings.xml create mode 100644 .idea/misc.xml create mode 100644 releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml diff --git a/.idea/QtSettings.xml b/.idea/QtSettings.xml new file mode 100644 index 0000000..0248d0c --- /dev/null +++ b/.idea/QtSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b76fe5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/pgLab.iml b/.idea/pgLab.iml index 6102194..4c94235 100644 --- a/.idea/pgLab.iml +++ b/.idea/pgLab.iml @@ -1,8 +1,2 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp index a8bcae1..c30a1bc 100644 --- a/pglab/crud/CrudModel.cpp +++ b/pglab/crud/CrudModel.cpp @@ -244,6 +244,7 @@ void CrudModel::loadData() void CrudModel::loadIntoModel(std::shared_ptr data) { beginResetModel(); + bool was_dirty = hasPendingChanges(); m_pendingRowList.clear(); m_roData = data; initializeColumnList(); @@ -253,6 +254,8 @@ void CrudModel::loadIntoModel(std::shared_ptr data) if (m_wholeTable) appendNewRowInternal(); endResetModel(); + if (was_dirty) + emit pendingChangesChanged(false); } void CrudModel::initRowMapping() @@ -321,7 +324,10 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol s.clear(); val = s; } + bool was_dirty = hasPendingChanges(); m_pendingRowList.setValue(col, row_mapping.rowKey, val); + if (!was_dirty) + emit pendingChangesChanged(true); emit dataChanged(index, index, QVector() << role); return true; @@ -611,14 +617,27 @@ void CrudModel::initializeColumnList() } } -bool CrudModel::submit() +bool CrudModel::saveChanges() { - return savePendingChanges(); + bool was_dirty = hasPendingChanges(); + bool ok = savePendingChanges(); + if (was_dirty != hasPendingChanges()) + emit pendingChangesChanged(hasPendingChanges()); + return ok; } -void CrudModel::revert() +void CrudModel::revertChanges() { + if (!hasPendingChanges()) + return; + m_pendingRowList.clear(); + for (auto &row_mapping : m_rowMapping) + row_mapping.pending = false; + + if (rowCount() > 0) + emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); + emit pendingChangesChanged(false); } void CrudModel::appendNewRow() @@ -678,7 +697,10 @@ std::tuple CrudModel::removeRows(const std::set // If something goes wrong after this commit we should reload contents of model } // Then from model + bool was_dirty = hasPendingChanges(); RemoveRangesOfRowsFromModel(row_ranges); + if (was_dirty != hasPendingChanges()) + emit pendingChangesChanged(hasPendingChanges()); return { true, "" }; } catch (const Pgsql::PgResultError &error) @@ -713,6 +735,8 @@ void CrudModel::RemoveRangeOfRowsFromModel(IntegerRange range) beginRemoveRows(QModelIndex(), range.start(), range.end() - 1); SCOPE_EXIT { endRemoveRows(); }; auto first = m_rowMapping.begin() + range.start(); + for (auto iter = first; iter != first + range.length(); ++iter) + m_pendingRowList.m_rows.erase(iter->rowKey); m_rowMapping.erase(first, first + range.length()); } diff --git a/pglab/crud/CrudModel.h b/pglab/crud/CrudModel.h index d05b6c0..485b6c2 100644 --- a/pglab/crud/CrudModel.h +++ b/pglab/crud/CrudModel.h @@ -46,12 +46,21 @@ public: void loadData(); + /// True when there are edits that have not been saved to the database + bool hasPendingChanges() const { return !m_pendingRowList.m_rows.empty(); } + /// Writes all pending rows to the database, rows that fail stay pending + bool saveChanges(); + /// Discards all pending edits, the grid shows the last saved values again + void revertChanges(); + std::tuple removeRows(const std::set> &row_ranges); bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; +signals: + /// Emitted when the model goes from clean to having pending changes or back + void pendingChangesChanged(bool hasChanges); + public slots: - virtual bool submit() override; - virtual void revert() override; private: diff --git a/pglab/crud/CrudTab.cpp b/pglab/crud/CrudTab.cpp index 88b0ac2..8bb44b0 100644 --- a/pglab/crud/CrudTab.cpp +++ b/pglab/crud/CrudTab.cpp @@ -60,6 +60,13 @@ void CrudTab::setConfig(Oid oid) //std::shared_ptr db, const PgCla void CrudTab::refresh() { + if (m_crudModel->hasPendingChanges()) { + auto res = QMessageBox::question(this, "pgLab", + tr("There are unsaved changes. Refreshing discards them. Do you want to continue?"), + QMessageBox::Yes, QMessageBox::No); + if (res != QMessageBox::Yes) + return; + } m_crudModel->loadData(); } diff --git a/pglab/crud/DataGridWidget.cpp b/pglab/crud/DataGridWidget.cpp index 64f19a8..65708f7 100644 --- a/pglab/crud/DataGridWidget.cpp +++ b/pglab/crud/DataGridWidget.cpp @@ -1,9 +1,13 @@ #include "DataGridWidget.h" +#include "CrudModel.h" #include "CustomDataRole.h" #include "ResultTableModelUtil.h" #include "util/PgLabItemDelegate.h" +#include #include +#include #include +#include #include @@ -13,6 +17,25 @@ DataGridWidget::DataGridWidget(QWidget *parent) auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); + m_toolbar = new QToolBar(this); + m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + layout->addWidget(m_toolbar); + + m_actSave = new QAction(style()->standardIcon(QStyle::SP_DialogSaveButton), tr("Save"), this); + m_actSave->setShortcut(QKeySequence::Save); + m_actSave->setShortcutContext(Qt::WidgetWithChildrenShortcut); + m_actSave->setToolTip(tr("Save all pending changes to the database (Ctrl+S)")); + m_actSave->setEnabled(false); + addAction(m_actSave); + m_toolbar->addAction(m_actSave); + connect(m_actSave, &QAction::triggered, this, &DataGridWidget::onSave); + + m_actRevert = new QAction(style()->standardIcon(QStyle::SP_DialogResetButton), tr("Revert"), this); + m_actRevert->setToolTip(tr("Discard all pending changes")); + m_actRevert->setEnabled(false); + m_toolbar->addAction(m_actRevert); + connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert); + m_view = new QTableView(this); layout->addWidget(m_view); @@ -27,8 +50,52 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model) m_proxy = new QSortFilterProxyModel(this); m_proxy->setSortRole(CustomSortRole); } + disconnect(m_pendingChangesConnection); m_proxy->setSourceModel(model); m_view->setModel(m_proxy); m_view->setSortingEnabled(true); m_proxy->sort(0, Qt::AscendingOrder); + + auto crud = qobject_cast(model); + m_toolbar->setVisible(crud != nullptr); + if (crud) { + m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged); + onPendingChangesChanged(crud->hasPendingChanges()); + } + else { + onPendingChangesChanged(false); + } +} + +void DataGridWidget::onPendingChangesChanged(bool hasChanges) +{ + m_actSave->setEnabled(hasChanges); + m_actRevert->setEnabled(hasChanges); +} + +void DataGridWidget::onSave() +{ + auto crud = qobject_cast(m_proxy->sourceModel()); + if (!crud) + return; + + commitOpenEditor(); + crud->saveChanges(); +} + +void DataGridWidget::onRevert() +{ + auto crud = qobject_cast(m_proxy->sourceModel()); + if (!crud) + return; + + commitOpenEditor(); + crud->revertChanges(); +} + +void DataGridWidget::commitOpenEditor() +{ + // Moving focus away from the editor makes the delegate commit its value, a toolbar click does not take focus + if (m_view->indexWidget(m_view->currentIndex())) + m_view->setFocus(); } diff --git a/pglab/crud/DataGridWidget.h b/pglab/crud/DataGridWidget.h index 11d6745..d605053 100644 --- a/pglab/crud/DataGridWidget.h +++ b/pglab/crud/DataGridWidget.h @@ -4,8 +4,10 @@ #include class QAbstractItemModel; +class QAction; class QSortFilterProxyModel; class QTableView; +class QToolBar; /** Sortable and editable grid shared by the CRUD tab and the query result tab. */ @@ -20,9 +22,21 @@ public: QTableView *view() const { return m_view; } QSortFilterProxyModel *proxy() const { return m_proxy; } +private slots: + void onPendingChangesChanged(bool hasChanges); + void onSave(); + void onRevert(); + private: + /// Closes an open cell editor so its value is written to the model before saving or reverting + void commitOpenEditor(); + QTableView *m_view = nullptr; QSortFilterProxyModel *m_proxy = nullptr; + QToolBar *m_toolbar = nullptr; + QAction *m_actSave = nullptr; + QAction *m_actRevert = nullptr; + QMetaObject::Connection m_pendingChangesConnection; }; #endif // DATAGRIDWIDGET_H diff --git a/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml b/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml new file mode 100644 index 0000000..ebfdb20 --- /dev/null +++ b/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Data grids in the table editor and in query results now have a toolbar + with :menuselection:`Save` and :menuselection:`Revert` buttons, Save can + also be triggered with :kbd:`Ctrl+S`. Both buttons are only enabled while + there are unsaved changes. +other: + - | + Edits in data grids are no longer saved automatically when a cell editor + is closed or when :kbd:`Escape` is pressed. Use the :menuselection:`Save` + button to write changes to the database, or :menuselection:`Revert` to + discard them. Refreshing a table with unsaved changes asks for confirmation.