Switch to cmake + vcpkg, builds on linux, windows needs verification.
Code has been modified to be portable.
This commit is contained in:
parent
c1a5ac1aad
commit
49f6619a4e
34 changed files with 678 additions and 327 deletions
30
CMakeLists.txt
Normal file
30
CMakeLists.txt
Normal file
|
|
@ -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()
|
||||||
38
README.md
38
README.md
|
|
@ -11,3 +11,41 @@ The release notes use collected from git using [reno](https://docs.openstack.org
|
||||||
- googletest
|
- googletest
|
||||||
- libpq
|
- 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,31 @@
|
||||||
set(CMAKE_AUTOMOC ON)
|
|
||||||
|
|
||||||
add_library(core STATIC
|
add_library(core STATIC
|
||||||
BackupFormatModel.cpp
|
|
||||||
CsvWriter.cpp
|
|
||||||
my_boost_assert_handler.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
|
target_link_libraries(core PUBLIC
|
||||||
boost
|
Qt6::Core
|
||||||
botan
|
|
||||||
Qt5::Core
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(core INTERFACE
|
target_include_directories(core PUBLIC
|
||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(core PROPERTIES
|
set_target_properties(core PROPERTIES
|
||||||
CXX_STANDARD 14
|
CXX_STANDARD 20
|
||||||
CXX_STANDARD_REQUIRED ON
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,115 @@
|
||||||
cmake_minimum_required(VERSION 3.2)
|
set(PGLAB_SOURCES
|
||||||
project(pglab)
|
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(PGLAB_FORMS
|
||||||
set(CMAKE_AUTOUIC ON)
|
ConnectionManagerWindow.ui
|
||||||
set(CMAKE_AUTORCC ON)
|
CreateDatabaseDialog.ui
|
||||||
|
DatabaseWindow.ui
|
||||||
|
tuplesresultwidget.ui
|
||||||
|
querytool/QueryTab.ui
|
||||||
|
ProcessStdioWidget.ui
|
||||||
|
crud/CrudTab.ui
|
||||||
|
CodeGenerator.ui
|
||||||
|
widgets/SingleRecordWidget.ui
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(pglab
|
add_executable(pglab
|
||||||
ASyncDBConnection.cpp
|
${PGLAB_SOURCES}
|
||||||
ASyncWindow.cpp
|
${PGLAB_FORMS}
|
||||||
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
|
|
||||||
resources.qrc
|
resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(pglab PROPERTIES
|
if(WIN32)
|
||||||
CXX_STANDARD 14
|
set_target_properties(pglab PROPERTIES WIN32_EXECUTABLE ON)
|
||||||
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")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries( pglab
|
target_link_libraries(pglab PRIVATE
|
||||||
core
|
pglablib
|
||||||
pgsql
|
pgsql
|
||||||
boost-system
|
core
|
||||||
Qt5::Widgets
|
Qt6::Core
|
||||||
pthread
|
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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
|
||||||
#include "catalog\PgDatabase.h"
|
#include "catalog/PgDatabase.h"
|
||||||
|
|
||||||
class PgDatabaseCatalog;
|
class PgDatabaseCatalog;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>CodeEditor</class>
|
<class>CodeEditor</class>
|
||||||
<extends>QPlainTextEdit</extends>
|
<extends>QPlainTextEdit</extends>
|
||||||
<header>codeeditor\CodeEditor.h</header>
|
<header>codeeditor/CodeEditor.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ QVariant DatabasesTableModel::getData(const QModelIndex &index) const
|
||||||
break;
|
break;
|
||||||
case SizeCol:
|
case SizeCol:
|
||||||
if (d.size.totalBytes >= 0)
|
if (d.size.totalBytes >= 0)
|
||||||
v = d.size.totalBytes;
|
v = QVariant::fromValue(d.size.totalBytes);
|
||||||
break;
|
break;
|
||||||
case AclCol:
|
case AclCol:
|
||||||
v = db.aclString();
|
v = db.aclString();
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ QVariant IndexModel::getData(const QModelIndex &index) const
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SizeCol:
|
case SizeCol:
|
||||||
v = dat.sizeBytes;
|
v = QVariant::fromValue(dat.sizeBytes);
|
||||||
break;
|
break;
|
||||||
case ExplicitIndexCol:
|
case ExplicitIndexCol:
|
||||||
v = !dat.isSupportingIndex();
|
v = !dat.isSupportingIndex();
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,12 @@ QVariant SequenceModel::data(const QModelIndex &index, int role) const
|
||||||
case NameCol: return seq.objectName();
|
case NameCol: return seq.objectName();
|
||||||
case SchemaCol: return seq.nsName();
|
case SchemaCol: return seq.nsName();
|
||||||
case OwnerCol: return seq.ownerName();
|
case OwnerCol: return seq.ownerName();
|
||||||
case LastCol: return seq.last;
|
case LastCol: return QVariant::fromValue(seq.last);
|
||||||
case StartCol: return seq.start;
|
case StartCol: return QVariant::fromValue(seq.start);
|
||||||
case MinCol: return seq.min;
|
case MinCol: return QVariant::fromValue(seq.min);
|
||||||
case MaxCol: return seq.max;
|
case MaxCol: return QVariant::fromValue(seq.max);
|
||||||
case IncrementCol: return seq.increment;
|
case IncrementCol: return QVariant::fromValue(seq.increment);
|
||||||
case CacheCol: return seq.cache;
|
case CacheCol: return QVariant::fromValue(seq.cache);
|
||||||
case CycledCol: return seq.cycled;
|
case CycledCol: return seq.cycled;
|
||||||
case CalledCol: return seq.called;
|
case CalledCol: return seq.called;
|
||||||
case AclCol: return seq.aclString();
|
case AclCol: return seq.aclString();
|
||||||
|
|
|
||||||
|
|
@ -178,10 +178,10 @@ QVariant TablesTableModel::getData(const QModelIndex &index) const
|
||||||
case OptionsCol: break;
|
case OptionsCol: break;
|
||||||
case AclCol: return t.aclString();
|
case AclCol: return t.aclString();
|
||||||
case CommentCol: return t.description;
|
case CommentCol: return t.description;
|
||||||
case TotalSizeCol: return s.oid == 0 ? QVariant() : s.totalBytes;
|
case TotalSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.totalBytes);
|
||||||
case TableSizeCol: return s.oid == 0 ? QVariant() : s.totalBytes - s.indexBytes - s.toastBytes;
|
case TableSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.totalBytes - s.indexBytes - s.toastBytes);
|
||||||
case IndexSizeCol: return s.oid == 0 ? QVariant() : s.indexBytes;
|
case IndexSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.indexBytes);
|
||||||
case ToastSizeCol: return s.oid == 0 ? QVariant() : s.toastBytes;
|
case ToastSizeCol: return s.oid == 0 ? QVariant() : QVariant::fromValue(s.toastBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ QVariant StringToVariant(const std::string &str, Oid oid)
|
||||||
|
|
||||||
case Pgsql::oid_oid:
|
case Pgsql::oid_oid:
|
||||||
case Pgsql::int8_oid:
|
case Pgsql::int8_oid:
|
||||||
return v.operator int64_t();
|
return QVariant::fromValue(static_cast<int64_t>(v));
|
||||||
|
|
||||||
case Pgsql::bool_oid:
|
case Pgsql::bool_oid:
|
||||||
return v.operator bool();
|
return v.operator bool();
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ if (role == Qt::DisplayRole) {
|
||||||
result = item->estimateError();
|
result = item->estimateError();
|
||||||
break;
|
break;
|
||||||
case c_ColumnRowCount:
|
case c_ColumnRowCount:
|
||||||
result = item->actualRows;
|
result = QVariant::fromValue(item->actualRows);
|
||||||
break;
|
break;
|
||||||
case c_ColumnLoops:
|
case c_ColumnLoops:
|
||||||
result = item->actualLoops;
|
result = item->actualLoops;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
#include "ASyncDBConnection.h"
|
#include "ASyncDBConnection.h"
|
||||||
#include "ScopeGuard.h"
|
#include "ScopeGuard.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "Pgsql_PgException.h"
|
#include "Pgsql_PgException.h"
|
||||||
#include "Win32Event.h"
|
#include <atomic>
|
||||||
#include "WaitHandleList.h"
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <WinSock2.h>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QEventLoop>
|
||||||
|
#include <QSocketNotifier>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
@ -45,10 +46,6 @@ public:
|
||||||
struct t_Command {
|
struct t_Command {
|
||||||
std::mutex m_mutex;
|
std::mutex m_mutex;
|
||||||
t_CommandQueue m_queue;
|
t_CommandQueue m_queue;
|
||||||
Win32Event m_newEvent;
|
|
||||||
t_Command()
|
|
||||||
: m_newEvent(Win32Event::Reset::Auto, Win32Event::Initial::Clear)
|
|
||||||
{}
|
|
||||||
} m_commandQueue;
|
} m_commandQueue;
|
||||||
|
|
||||||
ConnectionConfig m_config;
|
ConnectionConfig m_config;
|
||||||
|
|
@ -64,18 +61,29 @@ public:
|
||||||
|
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
|
/// Wakes the worker thread when it is waiting in its event loop
|
||||||
|
void wake();
|
||||||
|
|
||||||
private:
|
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;
|
ASyncDBConnection *asyncConnObject = nullptr;
|
||||||
|
|
||||||
/// \todo Implement new method to stop the thread
|
std::atomic<bool> terminateRequested{false}; ///< is set when the thread should stop
|
||||||
Win32Event m_stopEvent;
|
std::mutex m_wakeMutex;
|
||||||
|
QEventLoop *m_loop = nullptr; ///< loop the worker blocks in, guarded by m_wakeMutex
|
||||||
Pgsql::Connection m_connection;
|
Pgsql::Connection m_connection;
|
||||||
bool terminateRequested = false; ///< is set when the thread should stop
|
|
||||||
bool m_terminated = true;
|
bool m_terminated = true;
|
||||||
Pgsql::Canceller m_canceller;
|
Pgsql::Canceller m_canceller;
|
||||||
QElapsedTimer m_timer;
|
QElapsedTimer m_timer;
|
||||||
|
|
||||||
|
|
||||||
|
bool hasQueuedCommand();
|
||||||
|
WaitResult waitForSocket(QSocketNotifier::Type type, int timeout_ms, bool wakeOnNewCommand);
|
||||||
bool makeConnection();
|
bool makeConnection();
|
||||||
void communicate();
|
void communicate();
|
||||||
|
|
||||||
|
|
@ -97,7 +105,6 @@ private:
|
||||||
|
|
||||||
ASyncDBConnectionThread::ASyncDBConnectionThread(ASyncDBConnection *asco)
|
ASyncDBConnectionThread::ASyncDBConnectionThread(ASyncDBConnection *asco)
|
||||||
: asyncConnObject(asco)
|
: asyncConnObject(asco)
|
||||||
, m_stopEvent(Win32Event::Reset::Manual, Win32Event::Initial::Clear)
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ASyncDBConnectionThread::run()
|
void ASyncDBConnectionThread::run()
|
||||||
|
|
@ -154,6 +161,59 @@ bool ASyncDBConnectionThread::cancel()
|
||||||
return m_canceller.cancel(nullptr);
|
return m_canceller.cancel(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ASyncDBConnectionThread::hasQueuedCommand()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lg(m_wakeMutex);
|
||||||
|
m_loop = nullptr;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ASyncDBConnectionThread::wake()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(m_wakeMutex);
|
||||||
|
if (m_loop) {
|
||||||
|
QMetaObject::invokeMethod(m_loop, [loop = m_loop] { loop->quit(); }, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool ASyncDBConnectionThread::makeConnection()
|
bool ASyncDBConnectionThread::makeConnection()
|
||||||
{
|
{
|
||||||
using namespace std::literals::chrono_literals;
|
using namespace std::literals::chrono_literals;
|
||||||
|
|
@ -165,30 +225,14 @@ bool ASyncDBConnectionThread::makeConnection()
|
||||||
bool ok = m_connection.connectStart(conn_string); //keywords, values);
|
bool ok = m_connection.connectStart(conn_string); //keywords, values);
|
||||||
auto start = std::chrono::steady_clock::now();
|
auto start = std::chrono::steady_clock::now();
|
||||||
if (ok && m_connection.status() != CONNECTION_BAD) {
|
if (ok && m_connection.status() != CONNECTION_BAD) {
|
||||||
Win32Event socket_event(Win32Event::Reset::Auto, Win32Event::Initial::Clear);
|
QSocketNotifier::Type type = QSocketNotifier::Write;
|
||||||
|
|
||||||
long fd = FD_WRITE;
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// poll till complete or failed (we can get an abort command)
|
// poll till complete or failed (we can get an abort command)
|
||||||
int sock = m_connection.socket();
|
auto elapsed = std::chrono::steady_clock::now() - start;
|
||||||
WSAEventSelect(sock, socket_event.handle(), fd);
|
auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(30s - elapsed).count();
|
||||||
WaitHandleList whl;
|
WaitResult res = waitForSocket(type, remaining > 0 ? static_cast<int>(remaining) : 0, false);
|
||||||
auto wait_result_socket_event = whl.add(socket_event);
|
|
||||||
auto wait_result_stop = whl.add(m_stopEvent);
|
|
||||||
|
|
||||||
auto nu = std::chrono::steady_clock::now();
|
if (res == WaitResult::Socket) {
|
||||||
std::chrono::duration<float, std::milli> 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) {
|
|
||||||
auto poll_state = m_connection.connectPoll();
|
auto poll_state = m_connection.connectPoll();
|
||||||
if (poll_state == PGRES_POLLING_OK) {
|
if (poll_state == PGRES_POLLING_OK) {
|
||||||
qDebug() << "ASyncDBConnection Connected";
|
qDebug() << "ASyncDBConnection Connected";
|
||||||
|
|
@ -202,17 +246,21 @@ bool ASyncDBConnectionThread::makeConnection()
|
||||||
}
|
}
|
||||||
else if (poll_state == PGRES_POLLING_READING) {
|
else if (poll_state == PGRES_POLLING_READING) {
|
||||||
doStateCallback({ ASyncDBConnection::State::Connecting, "Negotiating" });
|
doStateCallback({ ASyncDBConnection::State::Connecting, "Negotiating" });
|
||||||
fd = FD_READ;
|
type = QSocketNotifier::Read;
|
||||||
}
|
}
|
||||||
else if (poll_state == PGRES_POLLING_WRITING) {
|
else if (poll_state == PGRES_POLLING_WRITING) {
|
||||||
doStateCallback({ ASyncDBConnection::State::Connecting, "Negotiating" });
|
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();
|
m_connection.close();
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
} // end while (true)
|
} // end while (true)
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +300,7 @@ void ASyncDBConnectionThread::communicate()
|
||||||
void ASyncDBConnectionThread::stop()
|
void ASyncDBConnectionThread::stop()
|
||||||
{
|
{
|
||||||
terminateRequested = true;
|
terminateRequested = true;
|
||||||
m_stopEvent.set();
|
wake();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ASyncDBConnectionThread::doStateCallback(ASyncDBConnection::StateData state)
|
void ASyncDBConnectionThread::doStateCallback(ASyncDBConnection::StateData state)
|
||||||
|
|
@ -264,34 +312,18 @@ void ASyncDBConnectionThread::doStateCallback(ASyncDBConnection::StateData state
|
||||||
|
|
||||||
bool ASyncDBConnectionThread::waitForAndSendCommand()
|
bool ASyncDBConnectionThread::waitForAndSendCommand()
|
||||||
{
|
{
|
||||||
SOCKET sock = static_cast<SOCKET>(m_connection.socket());
|
if (hasQueuedCommand()) {
|
||||||
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) {
|
|
||||||
doNewCommand();
|
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
|
// Note if it was stop we can just return and function
|
||||||
// above will stop looping because terminateRequested has been set too by stop
|
// above will stop looping because terminateRequested has been set too by stop
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -347,42 +379,16 @@ bool ASyncDBConnectionThread::consumeResultInput()
|
||||||
|
|
||||||
void ASyncDBConnectionThread::waitForResult()
|
void ASyncDBConnectionThread::waitForResult()
|
||||||
{
|
{
|
||||||
SOCKET sock = static_cast<SOCKET>(m_connection.socket());
|
|
||||||
Win32Event socket_event(Win32Event::Reset::Manual, Win32Event::Initial::Clear);
|
|
||||||
|
|
||||||
long fd = FD_READ | FD_CLOSE;
|
|
||||||
|
|
||||||
bool finished = false;
|
bool finished = false;
|
||||||
while ( ! finished) {
|
while ( ! finished) {
|
||||||
WSAEventSelect(sock, socket_event.handle(), fd);
|
WaitResult res = waitForSocket(QSocketNotifier::Read, -1, false);
|
||||||
|
|
||||||
WaitHandleList whl;
|
if (res == WaitResult::Socket) {
|
||||||
auto wait_result_socket = whl.add(socket_event);
|
if (consumeResultInput()) {
|
||||||
auto wait_result_stop = whl.add(m_stopEvent);
|
finished = true;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
// Send cancel, close connection and terminate thread
|
||||||
cancel();
|
cancel();
|
||||||
doStateCallback(ASyncDBConnection::State::Terminating);
|
doStateCallback(ASyncDBConnection::State::Terminating);
|
||||||
|
|
@ -455,8 +461,8 @@ bool ASyncDBConnection::send(const std::string &command, on_result_callback on_r
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(m_threadData->m_commandQueue.m_mutex);
|
std::lock_guard<std::mutex> lg(m_threadData->m_commandQueue.m_mutex);
|
||||||
m_threadData->m_commandQueue.m_queue.emplace(command, on_result);
|
m_threadData->m_commandQueue.m_queue.emplace(command, on_result);
|
||||||
m_threadData->m_commandQueue.m_newEvent.set();
|
|
||||||
}
|
}
|
||||||
|
m_threadData->wake();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,8 +474,8 @@ bool ASyncDBConnection::send(const std::string &command, Pgsql::Params params, o
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(m_threadData->m_commandQueue.m_mutex);
|
std::lock_guard<std::mutex> 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_queue.emplace(command, std::move(params), on_result);
|
||||||
m_threadData->m_commandQueue.m_newEvent.set();
|
|
||||||
}
|
}
|
||||||
|
m_threadData->wake();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
class ASyncDBConnectionThread;
|
class ASyncDBConnectionThread;
|
||||||
/** \brief Class that handles asynchronous execution of queries.
|
/** \brief Class that handles asynchronous execution of queries.
|
||||||
|
|
@ -33,7 +34,7 @@ public:
|
||||||
|
|
||||||
class StateData {
|
class StateData {
|
||||||
public:
|
public:
|
||||||
State State;
|
enum State State;
|
||||||
QString Message;
|
QString Message;
|
||||||
|
|
||||||
StateData(enum State state, QString message = QString())
|
StateData(enum State state, QString message = QString())
|
||||||
|
|
|
||||||
187
pglablib/CMakeLists.txt
Normal file
187
pglablib/CMakeLists.txt
Normal file
|
|
@ -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
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(pglablib PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_STANDARD_REQUIRED ON
|
||||||
|
POSITION_INDEPENDENT_CODE ON
|
||||||
|
)
|
||||||
|
|
@ -200,7 +200,7 @@ QString quoteIdent(QString ident)
|
||||||
{
|
{
|
||||||
assert(ident.length() > 0);
|
assert(ident.length() > 0);
|
||||||
|
|
||||||
static const wchar_t dquote = L'"';
|
static const QChar dquote = u'"';
|
||||||
|
|
||||||
if (identNeedsQuotes(ident)) {
|
if (identNeedsQuotes(ident)) {
|
||||||
QString out;
|
QString out;
|
||||||
|
|
|
||||||
|
|
@ -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<DWORD>(m_waitHandles.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitResultValue WaitHandleList::add(Win32Event &e)
|
|
||||||
{
|
|
||||||
return add(e.handle());
|
|
||||||
}
|
|
||||||
|
|
||||||
DWORD WaitHandleList::count() const
|
|
||||||
{
|
|
||||||
return static_cast<DWORD>(m_waitHandles.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void WaitHandleList::clear()
|
|
||||||
{
|
|
||||||
m_waitHandles.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
WaitHandleList::operator const HANDLE*() const
|
|
||||||
{
|
|
||||||
return m_waitHandles.data();
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
#ifndef WAITHANDLELIST_H
|
|
||||||
#define WAITHANDLELIST_H
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
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<HANDLE> m_waitHandles;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // WAITHANDLELIST_H
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
#ifndef WIN32EVENT_H
|
|
||||||
#define WIN32EVENT_H
|
|
||||||
|
|
||||||
#include <WinSock2.h>
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
/** 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
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
#include "PgLanguage.h"
|
#include "PgLanguage.h"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
QString PgLanguage::createSql() const
|
QString PgLanguage::createSql() const
|
||||||
{
|
{
|
||||||
throw std::exception("PgLanguage::createSql() not implemented");
|
throw std::logic_error("PgLanguage::createSql() not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PgLanguage::dropSql() const
|
QString PgLanguage::dropSql() const
|
||||||
{
|
{
|
||||||
throw std::exception("PgLanguage::dropSql() not implemented");
|
throw std::logic_error("PgLanguage::dropSql() not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PgLanguage::typeName() const
|
QString PgLanguage::typeName() const
|
||||||
|
|
|
||||||
|
|
@ -48,15 +48,15 @@ namespace {
|
||||||
|
|
||||||
for(; index < len; index++) {
|
for(; index < len; index++) {
|
||||||
QChar ch = str[index];
|
QChar ch = str[index];
|
||||||
if (!double_quote && ch == L'\'')
|
if (!double_quote && ch == u'\'')
|
||||||
single_quote = !single_quote;
|
single_quote = !single_quote;
|
||||||
else if (!single_quote && ch == L'"')
|
else if (!single_quote && ch == u'"')
|
||||||
double_quote = !double_quote;
|
double_quote = !double_quote;
|
||||||
else if (!double_quote && !single_quote && ch == L'(')
|
else if (!double_quote && !single_quote && ch == u'(')
|
||||||
brackets++;
|
brackets++;
|
||||||
else if (!double_quote && !single_quote && ch == L')')
|
else if (!double_quote && !single_quote && ch == u')')
|
||||||
brackets--;
|
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)
|
if (index != start_array)
|
||||||
res.push_back(str.mid(start_array, index - 1).trimmed());
|
res.push_back(str.mid(start_array, index - 1).trimmed());
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ QString PgServerObject::alterOwnerSql(const QString& ident) const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void PgServerObject::setAcls(boost::optional<AclList> acls)
|
void PgServerObject::setAcls(std::optional<AclList> acls)
|
||||||
{
|
{
|
||||||
m_acls = std::move(acls);
|
m_acls = std::move(acls);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include "PgObject.h"
|
#include "PgObject.h"
|
||||||
#include "PgAcl.h"
|
#include "PgAcl.h"
|
||||||
#include <boost/optional.hpp>
|
#include <optional>
|
||||||
|
|
||||||
class PgAuthId;
|
class PgAuthId;
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ public:
|
||||||
* @brief setAcls
|
* @brief setAcls
|
||||||
* @param acls Important: pass empty optional when acl IS NULL, pass empty list for empty array
|
* @param acls Important: pass empty optional when acl IS NULL, pass empty list for empty array
|
||||||
*/
|
*/
|
||||||
void setAcls(boost::optional<AclList> acls);
|
void setAcls(std::optional<AclList> acls);
|
||||||
QString aclString() const;
|
QString aclString() const;
|
||||||
/**
|
/**
|
||||||
* @brief grantSql
|
* @brief grantSql
|
||||||
|
|
@ -53,7 +53,7 @@ protected:
|
||||||
private:
|
private:
|
||||||
Oid m_ownerOid = InvalidOid;
|
Oid m_ownerOid = InvalidOid;
|
||||||
const PgAuthId * m_owner = nullptr;
|
const PgAuthId * m_owner = nullptr;
|
||||||
boost::optional<AclList> m_acls;
|
std::optional<AclList> m_acls;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PGSERVEROBJECT_H
|
#endif // PGSERVEROBJECT_H
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
#define STRINGESCAPERULE_H
|
#define STRINGESCAPERULE_H
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <boost/optional.hpp>
|
|
||||||
|
|
||||||
enum class ConvertToNumericEscape {
|
enum class ConvertToNumericEscape {
|
||||||
Utf8, ///< Generates upto four escapes for 4 utf8 chars
|
Utf8, ///< Generates upto four escapes for 4 utf8 chars
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
std::string HumanReadableBytes(uint64_t bytes);
|
std::string HumanReadableBytes(uint64_t bytes);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
#include <botan/base64.h>
|
#include <botan/base64.h>
|
||||||
#include <botan/mac.h>
|
#include <botan/mac.h>
|
||||||
#include <botan/block_cipher.h>
|
#include <botan/block_cipher.h>
|
||||||
#include <boost/lexical_cast.hpp>
|
#include <botan/exceptn.h>
|
||||||
|
|
||||||
using namespace Botan;
|
using namespace Botan;
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ public:
|
||||||
* distinct key which is derived by hashing the wrapped key name with HMAC.
|
* distinct key which is derived by hashing the wrapped key name with HMAC.
|
||||||
*/
|
*/
|
||||||
const std::vector<uint8_t> wrapped_name =
|
const std::vector<uint8_t> wrapped_name =
|
||||||
nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()),
|
nist_key_wrap_padded(reinterpret_cast<const uint8_t*>(name.data()),
|
||||||
name.size(),
|
name.size(),
|
||||||
*m_cipher);
|
*m_cipher);
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ public:
|
||||||
secure_vector<uint8_t> get(const std::string& name, const std::string_view &wrapped_key) const
|
secure_vector<uint8_t> get(const std::string& name, const std::string_view &wrapped_key) const
|
||||||
{
|
{
|
||||||
const std::vector<uint8_t> wrapped_name =
|
const std::vector<uint8_t> wrapped_name =
|
||||||
nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()),
|
nist_key_wrap_padded(reinterpret_cast<const uint8_t*>(name.data()),
|
||||||
name.size(),
|
name.size(),
|
||||||
*m_cipher);
|
*m_cipher);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ namespace Botan {
|
||||||
|
|
||||||
class PasswordManagerException: public std::exception {
|
class PasswordManagerException: public std::exception {
|
||||||
public:
|
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 {
|
class PasswordManagerLockedException: public PasswordManagerException {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "ArrayParser.h"
|
#include "ArrayParser.h"
|
||||||
|
#include <cstring>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace Pgsql;
|
using namespace Pgsql;
|
||||||
|
|
|
||||||
48
pgsql/CMakeLists.txt
Normal file
48
pgsql/CMakeLists.txt
Normal file
|
|
@ -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
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(pgsql PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_STANDARD_REQUIRED ON
|
||||||
|
POSITION_INDEPENDENT_CODE ON
|
||||||
|
)
|
||||||
5
tests/CMakeLists.txt
Normal file
5
tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
find_package(GTest CONFIG REQUIRED)
|
||||||
|
|
||||||
|
add_subdirectory(mygtestutils)
|
||||||
|
add_subdirectory(PgsqlTests)
|
||||||
|
add_subdirectory(pglabtests)
|
||||||
24
tests/PgsqlTests/CMakeLists.txt
Normal file
24
tests/PgsqlTests/CMakeLists.txt
Normal file
|
|
@ -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)
|
||||||
19
tests/mygtestutils/CMakeLists.txt
Normal file
19
tests/mygtestutils/CMakeLists.txt
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
41
tests/pglabtests/CMakeLists.txt
Normal file
41
tests/pglabtests/CMakeLists.txt
Normal file
|
|
@ -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)
|
||||||
13
vcpkg.json
Normal file
13
vcpkg.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "pglab",
|
||||||
|
"version-string": "0.1.0",
|
||||||
|
"dependencies": [
|
||||||
|
"boost-container",
|
||||||
|
"boost-timer",
|
||||||
|
"boost-chrono",
|
||||||
|
"boost-assert",
|
||||||
|
"botan",
|
||||||
|
"libpq",
|
||||||
|
"gtest"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue