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/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/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/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/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..4c94235
--- /dev/null
+++ b/.idea/pgLab.iml
@@ -0,0 +1,2 @@
+
+
\ 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/.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).
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.
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/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/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/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/CMakeLists.txt b/pglab/CMakeLists.txt
index d6a1072..fb3d3f2 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
+ crud/DataGridWidget.cpp
+ querytool/QueryExplainModel.cpp
+ querytool/QueryParamListController.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
-
+
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..c30a1bc 100644
--- a/pglab/crud/CrudModel.cpp
+++ b/pglab/crud/CrudModel.cpp
@@ -4,11 +4,13 @@
#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"
#include
#include
+#include
#include
#include "Pgsql_oids.h"
#include "Pgsql_PgException.h"
@@ -39,12 +41,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)
@@ -150,7 +163,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();
@@ -205,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 {};
}
@@ -229,13 +244,18 @@ void CrudModel::loadData()
void CrudModel::loadIntoModel(std::shared_ptr data)
{
beginResetModel();
+ bool was_dirty = hasPendingChanges();
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();
+ if (was_dirty)
+ emit pendingChangesChanged(false);
}
void CrudModel::initRowMapping()
@@ -277,7 +297,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;
@@ -304,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;
@@ -312,22 +335,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 +374,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 +503,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 +520,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 +530,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 +547,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 +586,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,28 +600,51 @@ 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();
}
}
-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()
{
+ if (!m_wholeTable)
+ return;
+
int row = static_cast(m_rowMapping.size());
beginInsertRows(QModelIndex(), row, row);
appendNewRowInternal();
@@ -565,6 +662,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 +688,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);
}
@@ -598,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)
@@ -633,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 5a6beb3..485b6c2 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