From 306571c4ef8833092fb209a82557174cbbc57567 Mon Sep 17 00:00:00 2001 From: chatlanin Date: Sun, 31 May 2026 10:47:37 +0300 Subject: [PATCH] initial commit --- .gitignore | 6 ++ README.md | 6 ++ bin/main.cpp | 15 ++++ bin/meson.build | 7 ++ meson.build | 50 ++++++++++++++ run.sh | 22 ++++++ src/meson.build | 25 +++++++ src/pgxx/pgxx.hpp | 123 +++++++++++++++++++++++++++++++++ src/pgxx/pool.hpp | 110 +++++++++++++++++++++++++++++ subprojects/hack.wrap | 6 ++ subprojects/nlohmann_json.wrap | 10 +++ 11 files changed, 380 insertions(+) create mode 100755 .gitignore create mode 100755 README.md create mode 100644 bin/main.cpp create mode 100644 bin/meson.build create mode 100755 meson.build create mode 100755 run.sh create mode 100755 src/meson.build create mode 100644 src/pgxx/pgxx.hpp create mode 100644 src/pgxx/pool.hpp create mode 100755 subprojects/hack.wrap create mode 100644 subprojects/nlohmann_json.wrap diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..9fe0ab8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +.tmp +.cache +subprojects/* +!subprojects/hack.wrap +!subprojects/nlohmann_json.wrap diff --git a/README.md b/README.md new file mode 100755 index 0000000..b0a8b65 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Билиотека для работы с PostgreSQL + +Данный продукт имеет сугубо узкое направление деятельности и предназначен для обслуживания +подключений к БД из серера + +в разработке... diff --git a/bin/main.cpp b/bin/main.cpp new file mode 100644 index 0000000..8a37c9f --- /dev/null +++ b/bin/main.cpp @@ -0,0 +1,15 @@ +#include +#include "pgxx/pgxx.hpp" + +auto main(int argc, char* args[]) -> int +{ + const std::string password = "zHpQW0JaULlR1PugM2SRGrszBdRYGLL4SdIxg3trl3rSeaLJLAlDnCNs33RES+CX"; + const std::string main_conn = "host=localhost port=5432 dbname=main_db user=chatlanin password=" + password; + const std::string replica_conn = "host=localhost port=5433 dbname=main_db user=chatlanin password=" + password; + + auto& db = pgxx::database::instance(); + db.init("main", main_conn, 20); + db.init("replica", replica_conn, 10); + + return 0; +} diff --git a/bin/meson.build b/bin/meson.build new file mode 100644 index 0000000..e110ec2 --- /dev/null +++ b/bin/meson.build @@ -0,0 +1,7 @@ +executable( + meson.project_name(), + 'main.cpp', + dependencies : deps, + include_directories : inc, + cpp_args: args +) diff --git a/meson.build b/meson.build new file mode 100755 index 0000000..cdbace7 --- /dev/null +++ b/meson.build @@ -0,0 +1,50 @@ +project( + meson.current_source_dir().split('/').get(-1), + 'cpp', + version : run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip(), + default_options : [ + 'warning_level=1', + 'optimization=3', + 'cpp_std=c++20' +]) + +cc = meson.get_compiler('cpp') +if cc.get_id() == 'gcc' + add_project_arguments( + '-Wpedantic', + '-Wno-shadow', + '-Wno-maybe-uninitialized', + '-Wno-volatile', + '-Wno-unused-header', + language: 'cpp' + ) +endif + +# Общие флаги для GCC и Clang +add_project_arguments( + '-Wno-unused-but-set-variable', + '-Wno-comment', + '-Wno-unused-parameter', + '-Wno-unused-value', + '-Wno-missing-field-initializers', + '-Wno-narrowing', + '-Wno-deprecated-enum-enum-conversion', + '-Wno-format-security', + '-Wno-switch', + '-Wno-ignored-attributes', + '-Wno-unused-variable', + '-Wno-unused-function', + '-Wno-unknown-pragmas', + language: 'cpp' +) + +args = [] +inc = [] +deps = [ + dependency('libpqxx'), + subproject('hack').get_variable('hack_dep'), + subproject('nlohmann_json').get_variable('nlohmann_json_dep'), +] + +subdir('src') +subdir('bin') diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..47ec7fa --- /dev/null +++ b/run.sh @@ -0,0 +1,22 @@ +#!/bin/zsh + +PROJECT_NAME=$(basename $PWD) + +run() { + command meson compile -C build + if [[ -z "$1" ]]; then + cd build + ./tests/$PROJECT_NAME + cd .. + else + meson test $1 -C build + fi +} + +if [ -d "build" ]; then + run +else + command meson setup build + run +fi + diff --git a/src/meson.build b/src/meson.build new file mode 100755 index 0000000..22d41f8 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,25 @@ +inc += include_directories('.') + +sources = [ ] + +# Только если есть реальные .cpp файлы +if sources.length() > 0 + lib = library( + meson.project_name(), + include_directories : inc, + sources: sources, + dependencies : deps, + cpp_args: args + ) + + deps += declare_dependency( + include_directories: inc, + link_with: lib, + ) +else + # Header-only библиотека + deps += declare_dependency( + include_directories: inc, + dependencies: deps + ) +endif diff --git a/src/pgxx/pgxx.hpp b/src/pgxx/pgxx.hpp new file mode 100644 index 0000000..4a4fda6 --- /dev/null +++ b/src/pgxx/pgxx.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include "pool.hpp" + +namespace pgxx +{ + class database : public hack::patterns::singleton + { + friend hack::patterns::singleton; + + public: + ~database() = default; + + private: + database() = default; + std::map m_connections; + + public: + bool ready() { return !m_connections.empty(); } + + void init(const std::string& conn_name, const std::string& conn_str, std::size_t conn_count = 10, std::chrono::seconds timeout = std::chrono::seconds(30)) + { + try + { + pool p; + p.init(conn_str, conn_count, timeout); + m_connections.emplace(conn_name, std::move(p)); + hack::log()("make connection [", conn_name, "] completed"); + } + catch(hack::exception& ex) + { + hack::error()("1"); + } + catch(const std::exception& ex) + { + hack::error()("2"); + } + catch(...) + { + hack::error()("3"); + } + } + + // Для запросов без параметров + pqxx::result execute(const std::string& connection_name, const std::string& query) + { + auto it = m_connections.find(connection_name); + if (it == m_connections.end()) + { + hack::exception ex; + // ex.description("Connection pool not found"); + // ex.params("connection_name", connection_name); + throw ex; + } + + pool::guard conn(it->second); + pqxx::work work(*conn); + auto result = work.exec(query); + work.commit(); + + return result; + } + + // Для запросов с параметрами (БЕЗОПАСНО!) + template + pqxx::result execute(const std::string& connection_name, const std::string& query, Args&&... params) + { + auto it = m_connections.find(connection_name); + if (it == m_connections.end()) + { + hack::exception ex; + ex.description("Connection pool not found"); + // ex.params("connection_name", connection_name); + throw ex; + } + + pool::guard conn(it->second); + pqxx::work work(*conn); + auto result = work.exec_params(query, std::forward(params)...); + work.commit(); + + return result; + } + + // для транзакций (несколько запросов) + template + auto transaction(const std::string& connection_name, Func&& func) + { + auto it = m_connections.find(connection_name); + if (it == m_connections.end()) + { + hack::exception ex; + ex.description("Connection pool not found"); + // ex.params("connection_name", connection_name); + throw ex; + } + + pool::guard conn(it->second); + pqxx::work work(*conn); + + auto result = func(work); + work.commit(); + + return result; + } + + /* + Как использовать с транзакциями. + Но по факту просто красивая обертка можно просто все и без метода transaction + db.transaction("main", [&](pqxx::work& txn) { + auto res = txn.exec_params( "SELECT balance FROM accounts WHERE id = $1 FOR UPDATE", from_id); + double balance = res[0]["balance"].as(); + if (balance < amount) throw std::runtime_error("Insufficient funds"); + txn.exec_params( "UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, from_id); + txn.exec_params( "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to_id); + txn.exec_params( "INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, $3)", from_id, to_id, amount); + }); + */ + }; +} diff --git a/src/pgxx/pool.hpp b/src/pgxx/pool.hpp new file mode 100644 index 0000000..ac76518 --- /dev/null +++ b/src/pgxx/pool.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace pgxx +{ + class pool + { + public: + pool() = default; + pool(pool&& other) noexcept : m_conn_str(std::move(other.m_conn_str)) , m_timeout(other.m_timeout) , m_pool(std::move(other.m_pool)) {} + + pool(const pool&) = delete; + pool& operator=(const pool&) = delete; + pool& operator=(pool&&) = delete; + + public: + void init(const std::string& conn_str, std::size_t pool_size = 10, std::chrono::seconds timeout = std::chrono::seconds(30)) + { + m_conn_str = conn_str; + m_timeout = timeout; + for (size_t i = 0; i < pool_size; i++) + m_pool.push(create_connection()); + } + + class guard + { + public: + guard(pool& pool) : m_pool(pool), m_conn(pool.acquire()) + { + ensure_valid(); + } + + ~guard() + { + if (m_conn) m_pool.release(std::move(m_conn)); + } + + pqxx::connection* operator->() { return m_conn.get(); } + pqxx::connection& operator*() { return *m_conn.get(); } + + private: + void ensure_valid() + { + if (!m_conn->is_open()) + { + m_conn = m_pool.recreate_connection(); + if (!m_conn || !m_conn->is_open()) + throw std::runtime_error("Failed to recreate database connection"); + } + } + + private: + pool& m_pool; + std::unique_ptr m_conn; + }; + + private: + std::unique_ptr acquire() + { + std::unique_lock lock(m_mutex); + if (!m_cond.wait_for(lock, m_timeout, [this] { return !m_pool.empty(); })) + throw std::runtime_error("Connection pool timeout"); + auto conn = std::move(m_pool.front()); + m_pool.pop(); + return conn; + } + + void release(std::unique_ptr conn) + { + try + { + if (!conn->is_open()) conn = create_connection(); + std::lock_guard lock(m_mutex); + m_pool.push(std::move(conn)); + m_cond.notify_one(); + } + catch (...) + { + // Подавляем исключения, чтобы не потерять соединение + std::lock_guard lock(m_mutex); + m_pool.push(create_connection()); + m_cond.notify_one(); + } + } + + std::unique_ptr create_connection() + { + return std::make_unique(m_conn_str); + } + + std::unique_ptr recreate_connection() + { + return create_connection(); + } + + private: + std::string m_conn_str; + std::chrono::seconds m_timeout; + std::queue> m_pool; + std::mutex m_mutex; + std::condition_variable m_cond; + }; +} diff --git a/subprojects/hack.wrap b/subprojects/hack.wrap new file mode 100755 index 0000000..8b65299 --- /dev/null +++ b/subprojects/hack.wrap @@ -0,0 +1,6 @@ +[wrap-git] +url = https://gitcast.ru/chatlanin/hack.git +revision = master + +[provide] +hack = hack_dep diff --git a/subprojects/nlohmann_json.wrap b/subprojects/nlohmann_json.wrap new file mode 100644 index 0000000..bf5d700 --- /dev/null +++ b/subprojects/nlohmann_json.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = nlohmann_json-3.11.2 +lead_directory_missing = true +source_url = https://github.com/nlohmann/json/releases/download/v3.11.2/include.zip +source_filename = nlohmann_json-3.11.2.zip +source_hash = e5c7a9f49a16814be27e4ed0ee900ecd0092bfb7dbfca65b5a421b774dccaaed +wrapdb_version = 3.11.2-1 + +[provide] +nlohmann_json = nlohmann_json_dep