#pragma once #include "GPL.h" #include #include #include #include #include using michlib::GPL; using michlib::int_cast; using michlib::MString; using michlib::pointer_cast; class SQLiteConnection { public: using DBType = sqlite3*; using FuncType = std::function; private: static DBType db; static size_t count; static std::vector destructs; public: SQLiteConnection() { count++; if(db == nullptr) { MString oldprefix = GPL.UsePrefix("SQLITE"); MString name = GPL.ParameterSValue("db", ""); GPL.UsePrefix(oldprefix); auto ret = sqlite3_open(name.Buf(), &db); if(ret != SQLITE_OK) { sqlite3_close(db); db = nullptr; } } } SQLiteConnection([[maybe_unused]] const SQLiteConnection& sq): SQLiteConnection() {} SQLiteConnection(SQLiteConnection&&) = default; SQLiteConnection& operator=([[maybe_unused]] const SQLiteConnection& sq) { *this = {}; return *this; } SQLiteConnection& operator=(SQLiteConnection&&) = default; ~SQLiteConnection() { if(count == 0) michlib::errmessage("Destructor of SQLiteConnection called on count==0"); if(count > 1) count--; else { count = 0; if(db != nullptr) { for(const auto& f: destructs) f(db); sqlite3_close(db); } db = nullptr; } } static void AddDestructor(FuncType&& f) { destructs.emplace_back(std::move(f)); } operator DBType() const { return db; } static DBType GetDB() { return db; } explicit operator bool() const { return db != nullptr; } }; class PostgreSQLConnection { public: using DBType = PGconn*; using FuncType = std::function; private: static DBType conn; static size_t count; static std::vector destructs; public: PostgreSQLConnection() { count++; if(conn == nullptr) { MString oldprefix = GPL.UsePrefix("POSTGRES"); MString name = GPL.ParameterSValue("connection", ""); GPL.UsePrefix(oldprefix); conn = PQconnectdb(name.Buf()); if(PQstatus(conn) != CONNECTION_OK) { michlib::errmessage(PQerrorMessage(conn)); PQfinish(conn); conn = nullptr; } } } PostgreSQLConnection([[maybe_unused]] const PostgreSQLConnection& pq): PostgreSQLConnection() {} PostgreSQLConnection(PostgreSQLConnection&&) = default; PostgreSQLConnection& operator=([[maybe_unused]] const PostgreSQLConnection& pq) { *this = {}; return *this; } PostgreSQLConnection& operator=(PostgreSQLConnection&&) = default; ~PostgreSQLConnection() { if(count == 0) michlib::errmessage("Destructor of PostgreSQLConnection called on count==0"); if(count > 1) count--; else { count = 0; if(conn != nullptr) { for(const auto& f: destructs) f(conn); PQfinish(conn); } conn = nullptr; } } static void AddDestructor(FuncType&& f) { destructs.emplace_back(std::move(f)); } operator DBType() const { return conn; } static DBType GetDB() { return conn; } explicit operator bool() const { return conn != nullptr; } }; class GenericCache { public: virtual bool Put(const MString& key, const MString& value, size_t ttl) const = 0; virtual std::pair Get(const MString& key) const = 0; virtual ~GenericCache() {} }; class FakeCache: public GenericCache { public: virtual bool Put([[maybe_unused]] const MString& key, [[maybe_unused]] const MString& value, [[maybe_unused]] size_t ttl) const override { return false; } virtual std::pair Get([[maybe_unused]] const MString& key) const override { return {"", false}; } virtual ~FakeCache() override {} }; class SQLiteCache: public GenericCache { static bool regdest; SQLiteConnection db; public: bool Init() { if(!db) return false; if(!regdest) { // Create table sqlite3_stmt* sqst; int i; i = sqlite3_prepare_v2(db, "CREATE TABLE IF NOT EXISTS `cache`('key' TEXT PRIMARY KEY ON CONFLICT REPLACE NOT NULL ON CONFLICT FAIL, 'value' BLOB NOT NULL ON CONFLICT FAIL, " "'exptime' INTEGER NOT NULL ON CONFLICT FAIL) WITHOUT ROWID, STRICT;", -1, &sqst, 0); i = sqlite3_step(sqst); if(i != SQLITE_DONE) { sqlite3_finalize(sqst); return false; } sqlite3_finalize(sqst); sqlite3_busy_timeout(db, 1000); db.AddDestructor( [](SQLiteConnection::DBType db) { sqlite3_stmt* sqst = nullptr; int i = SQLITE_OK; if(i == SQLITE_OK) i = sqlite3_prepare_v2(db, "DELETE from `cache` WHERE exptime Get(const MString& key) const override { if(!*this) return {"", false}; sqlite3_stmt* sqst = nullptr; int i = SQLITE_OK; if(i == SQLITE_OK) i = sqlite3_prepare_v2(db, "SELECT value from `cache` WHERE key=?1 AND exptime>?2;", -1, &sqst, 0); if(i == SQLITE_OK) i = sqlite3_bind_text(sqst, 1, key.Buf(), -1, SQLITE_STATIC); if(i == SQLITE_OK) i = sqlite3_bind_int64(sqst, 2, time(nullptr)); if(i == SQLITE_OK) i = sqlite3_step(sqst); if(i == SQLITE_ROW) { auto p = sqlite3_column_blob(sqst, 0); auto sz = sqlite3_column_bytes(sqst, 0); if(p != nullptr) { MString out(p, sz); sqlite3_finalize(sqst); return {std::move(out), true}; } } sqlite3_finalize(sqst); return {"", false}; } virtual ~SQLiteCache() override = default; explicit operator bool() const { return db != nullptr; } }; class PostgreSQLHelpers { static constexpr time_t postgresepoch = 946648800; class PGResultRAIIDT { public: // TODO: make static void operator()(PGresult* res) { PQclear(res); } }; protected: PostgreSQLConnection conn; // Convert Postgres binary representation of timestamp to Unix epoch seconds. Microseconds ignored static time_t raw2epoch(time_t raw) { return Invert(raw) / 1000000 + postgresepoch; } // Convert Unix epoch time to Postres binary representation static time_t epoch2raw(time_t epoch) { return Invert((epoch - postgresepoch) * 1000000); } class PGresultRAII: public std::unique_ptr { public: PGresultRAII() = default; PGresultRAII(PGresult* res): std::unique_ptr(res) {} operator PGresult*() const { return get(); } }; bool CheckCon() const { if(!*this) return false; if(PQstatus(conn) == CONNECTION_OK) return true; PQreset(conn); return PQstatus(conn) == CONNECTION_OK; } template static D Invert(D d) { using michlib::int1; D out; Invert(&d, &out, sizeof(D)); return out; } static void Invert(const void* src, void* dst, size_t sz) { if(sz == 0) return; const int1* pin = pointer_cast(src); int1* pout = pointer_cast(dst); for(size_t i = 0; i < sz; i++) pout[sz - i - 1] = pin[i]; } public: explicit operator bool() const { return conn != nullptr; } }; class PostgreSQLCache: public GenericCache, public PostgreSQLHelpers { static bool regdest; public: bool Init() { if(!conn) return false; if(!regdest) { PGresultRAII res; // Create table res = PQexec(conn, "SET client_min_messages=WARNING;"); res = PQexec(conn, "CREATE TABLE IF NOT EXISTS cache(key TEXT PRIMARY KEY NOT NULL, value BYTEA, exptime TIMESTAMP(0) NOT NULL);"); if(PQresultStatus(res) != PGRES_COMMAND_OK) { michlib::errmessage(PQresStatus(PQresultStatus(res))); michlib::errmessage(PQerrorMessage(conn)); } res = PQexec(conn, "SET client_min_messages=NOTICE;"); conn.AddDestructor([](PostgreSQLConnection::DBType conn) { PGresultRAII res = PQexec(conn, "DELETE FROM cache WHERE exptime(ttl); michlib::int8 rinterval = Invert(interval); const char* params[] = {key.Buf(), value.Buf(), pointer_cast(&rinterval)}; int plens[] = {int_cast(key.Len()), int_cast(value.Len()), sizeof(rinterval)}; int pfor[] = {0, 1, 1}; PGresultRAII res = PQexecParams(conn, "INSERT INTO cache(key,value,exptime) VALUES($1,$2,localtimestamp + ($3::bigint ||' seconds')::interval)" "ON CONFLICT(key) DO UPDATE SET value=EXCLUDED.value, exptime=EXCLUDED.exptime;", 3, nullptr, params, plens, pfor, 1); if(PQresultStatus(res) != PGRES_COMMAND_OK) { michlib::errmessage(PQresStatus(PQresultStatus(res))); michlib::errmessage(PQerrorMessage(conn)); return false; } return true; } virtual std::pair Get(const MString& key) const override { if(!CheckCon()) return {"", false}; const char* params[] = {key.Buf()}; int plens[] = {int_cast(key.Len())}; int pfor[] = {0}; PGresultRAII res = PQexecParams(conn, "SELECT value from cache WHERE key=$1::text AND exptime>localtimestamp;", 1, nullptr, params, plens, pfor, 1); if(PQresultStatus(res) != PGRES_TUPLES_OK) { michlib::errmessage(PQresStatus(PQresultStatus(res))); michlib::errmessage(PQerrorMessage(conn)); return {"", false}; } else if(PQntuples(res) == 0) return {"", false}; MString val(PQgetvalue(res, 0, 0), PQgetlength(res, 0, 0)); return {std::move(val), true}; } virtual ~PostgreSQLCache() override = default; }; inline GenericCache* CreateCache(const MString& cachedesc) { auto i = cachedesc.GetPos(':'); auto name = i == 0 ? cachedesc : cachedesc.SubStr(1, i - 1); auto par = i == 0 ? "" : cachedesc.SubStr(i + 1, cachedesc.Len() - i); if(name == "no") return new FakeCache; if(name == "sqlite") { auto ret = new SQLiteCache; ret->Init(); if(*ret) return ret; delete ret; } if(name == "postgre" || name == "postgres" || name == "postgresql") { auto ret = new PostgreSQLCache; ret->Init(); if(*ret) return ret; delete ret; } return nullptr; }