diff --git a/CMakeLists.txt b/CMakeLists.txt index d7f38db5..6619fa02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,9 @@ option(OPENADS_WITH_TLS "Enable TLS transport (vendors mbedtls 3.6 LTS via Fetch # and nlohmann/json (MIT) at configure time via FetchContent. option(OPENADS_WITH_HTTP "Enable embedded HTTP web console (Studio) in openads_serverd / ace64.dll" ON) +# Optional MariaDB table driver (mariadb-connector-c). OFF by default. +option(OPENADS_WITH_MARIADB "Enable MariaDB-backed table driver (libmariadb)" OFF) + # Pull in mbedtls FIRST so our strict /WX -Werror flags don't bleed # into the upstream sources (C4200 zero-sized arrays etc). Each # OpenADS target adds the strict flags target-locally below via @@ -126,6 +129,59 @@ if(OPENADS_WITH_HTTP) message(STATUS "OpenADS: HTTP web console enabled (cpp-httplib + nlohmann/json)") endif() +if(OPENADS_WITH_MARIADB) + set(_openads_maria_linked FALSE) + set(OPENADS_LIBMARIADB_INCLUDE "" CACHE PATH "Directory containing mysql.h") + set(OPENADS_LIBMARIADB_LIBRARY "" CACHE FILEPATH "libmariadb import library (.lib/.a)") + if(OPENADS_LIBMARIADB_INCLUDE AND OPENADS_LIBMARIADB_LIBRARY) + set(_openads_maria_inc "${OPENADS_LIBMARIADB_INCLUDE}") + set(_openads_maria_lib "${OPENADS_LIBMARIADB_LIBRARY}") + endif() + if(NOT _openads_maria_lib) + if(DEFINED ENV{OPENADS_LIBMARIADB_LIBRARY}) + set(_openads_maria_lib "$ENV{OPENADS_LIBMARIADB_LIBRARY}") + endif() + if(DEFINED ENV{OPENADS_LIBMARIADB_INCLUDE}) + set(_openads_maria_inc "$ENV{OPENADS_LIBMARIADB_INCLUDE}") + endif() + endif() + if(NOT _openads_maria_lib OR NOT _openads_maria_inc) + if(DEFINED ENV{OPENADS_TOOLCHAIN_ROOT}) + set(_openads_toolchain_root "$ENV{OPENADS_TOOLCHAIN_ROOT}") + elseif(DEFINED OPENADS_TOOLCHAIN_ROOT) + set(_openads_toolchain_root "${OPENADS_TOOLCHAIN_ROOT}") + endif() + if(_openads_toolchain_root) + if(NOT _openads_maria_inc) + find_path(_openads_maria_inc mysql.h + PATHS "${_openads_toolchain_root}/mariadb/include" + NO_DEFAULT_PATH) + endif() + if(NOT _openads_maria_lib) + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + find_library(_openads_maria_lib NAMES libmariadb + PATHS "${_openads_toolchain_root}/mariadb/lib" + NO_DEFAULT_PATH) + else() + find_library(_openads_maria_lib NAMES libmariadb64 libmariadb + PATHS "${_openads_toolchain_root}/mariadb/lib" + NO_DEFAULT_PATH) + endif() + endif() + endif() + endif() + if(_openads_maria_inc AND _openads_maria_lib) + add_library(openads_libmariadb INTERFACE) + target_include_directories(openads_libmariadb INTERFACE "${_openads_maria_inc}") + target_link_libraries(openads_libmariadb INTERFACE "${_openads_maria_lib}") + set(_openads_maria_linked TRUE) + message(STATUS "OpenADS: MariaDB backend (libmariadb: ${_openads_maria_lib})") + endif() + if(NOT _openads_maria_linked) + message(FATAL_ERROR "OPENADS_WITH_MARIADB=ON but libmariadb was not found") + endif() +endif() + if(MSVC) add_compile_options(/W4 /permissive-) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8c996c24..bb30f706 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,6 +58,15 @@ add_library(openads_core STATIC network/mg_wire.cpp ) +if(OPENADS_WITH_MARIADB) + target_sources(openads_core PRIVATE + sql_backend/sql_common.cpp + sql_backend/maria_uri.cpp + sql_backend/maria_backend.cpp + sql_backend/maria_connection.cpp + ) +endif() + if(WIN32) target_sources(openads_core PRIVATE platform/file_win32.cpp @@ -98,6 +107,11 @@ if(OPENADS_WITH_TLS) MbedTLS::mbedtls MbedTLS::mbedx509 MbedTLS::mbedcrypto) endif() +if(OPENADS_WITH_MARIADB) + target_compile_definitions(openads_core PUBLIC OPENADS_WITH_MARIADB=1) + target_link_libraries(openads_core PUBLIC openads_libmariadb) +endif() + # ---------------------------------------------------------------------- # Drop-in replacement DLL: ace32.dll (x86) / ace64.dll (x64). # Harbour's contrib/rddads links against ace32.lib / ace64.lib import diff --git a/src/abi/ace_exports.cpp b/src/abi/ace_exports.cpp index bad7d68a..ecfabc40 100644 --- a/src/abi/ace_exports.cpp +++ b/src/abi/ace_exports.cpp @@ -36,6 +36,12 @@ #include "platform/time.h" #include "sql/parser.h" +#if defined(OPENADS_WITH_MARIADB) +#include "sql_backend/maria_connection.h" +#include "sql_backend/maria_index.h" +#include "sql_backend/maria_uri.h" +#endif + #include #include #include @@ -265,6 +271,82 @@ openads::network::RemoteIndex* get_remote_index(ADSHANDLE h) { h, HandleKind::RemoteIndex); } +#if defined(OPENADS_WITH_MARIADB) +std::unordered_map>& +maria_conns_map() { + static std::unordered_map> m; + return m; +} + +std::unordered_map>& +maria_tables_map() { + static std::unordered_map> m; + return m; +} + +openads::sql_backend::MariaTable* get_maria_table(ADSHANDLE h) { + auto& s = state(); + return s.registry.lookup( + h, HandleKind::MariaTable); +} + +std::unordered_map>& +maria_indexes_map() { + static std::unordered_map> m; + return m; +} + +openads::sql_backend::MariaIndex* get_maria_index(ADSHANDLE h) { + auto& s = state(); + return s.registry.lookup( + h, HandleKind::MariaIndex); +} + +std::size_t maria_field_index(openads::sql_backend::MariaTable* st, + UNSIGNED8* pucField) { + if (!st->fields_cached) { + if (st->conn == nullptr) { + return std::numeric_limits::max(); + } + auto r = st->conn->describe_table(st); + if (!r) return std::numeric_limits::max(); + } + { + auto p = reinterpret_cast(pucField); + if (p != 0 && p < 0x10000u) { + std::size_t one_based = static_cast(p); + if (one_based >= 1 && one_based <= st->fields.size()) { + return one_based - 1; + } + return std::numeric_limits::max(); + } + } + if (pucField == nullptr) { + return std::numeric_limits::max(); + } + std::string want = openads::abi::to_internal(pucField, 0); + for (auto& c : want) { + c = static_cast( + std::toupper(static_cast(c))); + } + for (std::size_t i = 0; i < st->fields.size(); ++i) { + std::string have = st->fields[i].name; + for (auto& c : have) { + c = static_cast( + std::toupper(static_cast(c))); + } + if (have == want) return i; + } + return std::numeric_limits::max(); +} +#endif // OPENADS_WITH_MARIADB + // Latch set by AdsSeekLast. AdsSeek consults this to suppress its // empty-key always-found quirk when called as part of rddads' // AdsSeekLast retry chain. AdsSkip clears it. @@ -802,6 +884,39 @@ UNSIGNED32 AdsConnect60(UNSIGNED8* pucServer, UNSIGNED16 /*usServerType*/, return ok(); } } +#if defined(OPENADS_WITH_MARIADB) + { + openads::sql_backend::MariaUri muri; + if (openads::sql_backend::parse_maria_uri(path, muri)) { + auto opened = openads::sql_backend::MariaConnection::open(muri); + if (!opened) return fail(opened.error()); + auto holder = std::make_unique( + std::move(opened).value()); + openads::sql_backend::MariaConnection* raw = holder.get(); + auto& s = state(); + std::lock_guard lk(s.mu); + Handle h = s.registry.register_object( + HandleKind::MariaConnection, raw); + maria_conns_map().emplace(h, std::move(holder)); + *phConnect = h; + return ok(); + } + } +#else + { + static constexpr const char* kMariaPrefixes[] = { + "mariadb://", "mysql://", + }; + for (const char* prefix : kMariaPrefixes) { + const auto plen = std::char_traits::length(prefix); + if (path.size() >= plen && path.compare(0, plen, prefix) == 0) { + return fail(openads::AE_FUNCTION_NOT_AVAILABLE, + "mariadb URI requires " + "OPENADS_WITH_MARIADB=ON"); + } + } + } +#endif auto opened = Connection::open(path); if (!opened) return fail(opened.error()); auto holder = std::make_unique(std::move(opened).value()); @@ -847,6 +962,20 @@ UNSIGNED32 AdsDisconnect(ADSHANDLE hConnect) { { auto& s_local = state(); std::lock_guard lk_local(s_local.mu); +#if defined(OPENADS_WITH_MARIADB) + if (auto* sc = s_local.registry.lookup( + hConnect, HandleKind::MariaConnection)) { + for (auto& kv : maria_tables_map()) { + if (kv.second && kv.second->conn == sc) { + kv.second->conn = nullptr; + } + } + sc->disconnect(); + maria_conns_map().erase(hConnect); + s_local.registry.release(hConnect); + return ok(); + } +#endif if (auto* rc = s_local.registry.lookup( hConnect, HandleKind::RemoteConnection)) { // Null out rt->conn on any open SQL cursors that reference this @@ -933,6 +1062,21 @@ UNSIGNED32 AdsOpenTable(ADSHANDLE hConnect, *phTable = gh; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* sc = s.registry.lookup( + hConnect, HandleKind::MariaConnection)) { + auto name = openads::abi::to_internal(pucName, 0); + auto tbl = sc->open_table(name); + if (!tbl) return fail(tbl.error()); + auto st = std::move(tbl).value(); + st->conn = sc; + Handle gh = s.registry.register_object( + HandleKind::MariaTable, st.get()); + maria_tables_map().emplace(gh, std::move(st)); + *phTable = gh; + return ok(); + } +#endif auto* conn = s.registry.lookup(hConnect, HandleKind::Connection); if (conn == nullptr) { ADSHANDLE def = get_or_create_default_connection(); @@ -1915,6 +2059,16 @@ UNSIGNED32 AdsCloseAllTables(void) { UNSIGNED32 AdsCloseTable(ADSHANDLE hTable) { { +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + (void)st; + auto& s2 = state(); + std::lock_guard lk2(s2.mu); + maria_tables_map().erase(hTable); + s2.registry.release(hTable); + return ok(); + } +#endif if (auto* rt = get_remote_table(hTable)) { // conn is nulled out by AdsDisconnect before the RemoteConnection // is freed; skip the wire close op if the connection is already gone. @@ -1957,6 +2111,16 @@ UNSIGNED32 AdsGotoTop(ADSHANDLE hTable) { if (!r) return fail(r.error()); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto r = st->conn->goto_top(st); + if (!r) return fail(r.error()); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, "unknown table"); auto r = t->goto_top(); @@ -1971,6 +2135,16 @@ UNSIGNED32 AdsGotoBottom(ADSHANDLE hTable) { if (!r) return fail(r.error()); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto r = st->conn->goto_bottom(st); + if (!r) return fail(r.error()); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, "unknown table"); auto r = t->goto_bottom(); @@ -2000,6 +2174,16 @@ UNSIGNED32 AdsSkip(ADSHANDLE hTable, SIGNED32 lRows) { if (!r) return fail(r.error()); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto r = st->conn->skip(st, lRows); + if (!r) return fail(r.error()); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, "unknown table"); auto r = t->skip(lRows); @@ -2016,6 +2200,18 @@ UNSIGNED32 AdsAtEOF(ADSHANDLE hTable, UNSIGNED16* pbAtEnd) { *pbAtEnd = r.value() ? 1 : 0; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (pbAtEnd == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto r = st->conn->at_eof(st); + if (!r) return fail(r.error()); + *pbAtEnd = r.value() ? 1 : 0; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t || pbAtEnd == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); *pbAtEnd = t->eof() ? 1 : 0; @@ -2030,6 +2226,17 @@ UNSIGNED32 AdsAtBOF(ADSHANDLE hTable, UNSIGNED16* pbAtBegin) { *pbAtBegin = r.value() ? 1 : 0; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto r = st->conn->at_bof(st); + if (!r) return fail(r.error()); + *pbAtBegin = r.value() ? 1 : 0; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); *pbAtBegin = t->bof() ? 1 : 0; @@ -2048,6 +2255,19 @@ UNSIGNED32 AdsGetNumFields(ADSHANDLE hTable, UNSIGNED16* pusFields) { *pusFields = static_cast(rt->fields.size()); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + if (!st->fields_cached) { + auto r = st->conn->describe_table(st); + if (!r) return fail(r.error()); + } + *pusFields = static_cast(st->fields.size()); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); if (auto* p = projection_for(hTable); p != nullptr) { @@ -2074,6 +2294,23 @@ UNSIGNED32 AdsGetFieldName(ADSHANDLE hTable, UNSIGNED16 usFieldNum, rt->fields[usFieldNum - 1].name); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + if (!st->fields_cached) { + auto r = st->conn->describe_table(st); + if (!r) return fail(r.error()); + } + if (usFieldNum == 0 || usFieldNum > st->fields.size()) { + return fail(openads::AE_COLUMN_NOT_FOUND, ""); + } + openads::abi::copy_to_caller(pucBuf, pusLen, + st->fields[usFieldNum - 1].name); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, "unknown table"); auto* p = projection_for(hTable); @@ -2151,6 +2388,16 @@ UNSIGNED32 AdsGetFieldType(ADSHANDLE hTable, UNSIGNED8* pucField, *pusType = rt->fields[i].type; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + auto i = maria_field_index(st, pucField); + if (i == std::numeric_limits::max()) { + return fail(openads::AE_COLUMN_NOT_FOUND, ""); + } + *pusType = st->fields[i].type; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); std::uint16_t idx = 0; @@ -2172,6 +2419,16 @@ UNSIGNED32 AdsGetFieldLength(ADSHANDLE hTable, UNSIGNED8* pucField, *pulLen = rt->fields[i].length; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + auto i = maria_field_index(st, pucField); + if (i == std::numeric_limits::max()) { + return fail(openads::AE_COLUMN_NOT_FOUND, ""); + } + *pulLen = st->fields[i].length; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); std::uint16_t idx = 0; @@ -2214,6 +2471,16 @@ UNSIGNED32 AdsGetFieldDecimals(ADSHANDLE hTable, UNSIGNED8* pucField, *pusDec = rt->fields[i].decimals; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + auto i = maria_field_index(st, pucField); + if (i == std::numeric_limits::max()) { + return fail(openads::AE_COLUMN_NOT_FOUND, ""); + } + *pusDec = st->fields[i].decimals; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); std::uint16_t idx = 0; @@ -2371,6 +2638,15 @@ UNSIGNED32 AdsGetRecordNum(ADSHANDLE hTable, UNSIGNED16 /*bFilterOption*/, *pulRecordNum = r.value(); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (!st->positioned || !st->row_valid) { + return fail(5026, "no current record"); + } + *pulRecordNum = static_cast(st->current_recno); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); *pulRecordNum = t->recno(); @@ -2397,6 +2673,24 @@ UNSIGNED32 AdsGetRecordCount(ADSHANDLE hTable, UNSIGNED16 /*bFilterOption*/, *pulRecordCount = rt->cached_rec_count; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (pulRecordCount == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + if (st->rec_count_cached) { + *pulRecordCount = st->cached_rec_count; + return ok(); + } + auto r = st->conn->record_count(st); + if (!r) return fail(r.error()); + st->cached_rec_count = r.value(); + st->rec_count_cached = true; + *pulRecordCount = st->cached_rec_count; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t || pulRecordCount == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); // M10.31 / M10.32 — when SQL has materialised a traversal sequence @@ -2467,6 +2761,27 @@ UNSIGNED32 AdsGetField(ADSHANDLE hTable, UNSIGNED8* pucField, openads::abi::copy_to_caller(pucBuf, pulLen, val); return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (pulLen == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); + if (st->conn == nullptr) { + return fail(openads::AE_INVALID_CONNECTION_HANDLE, ""); + } + auto fname = openads::abi::to_internal(pucField, 0); + bool is_null = false; + std::string val; + auto r = st->conn->read_field(st, fname, val, is_null); + if (!r) return fail(r.error()); + if (is_null) val.clear(); + auto fi = maria_field_index(st, pucField); + if (fi != std::numeric_limits::max() && + st->fields[fi].type == ADS_STRING) { + val = pad_char_field(std::move(val), st->fields[fi].length); + } + openads::abi::copy_to_caller(pucBuf, pulLen, val); + return ok(); + } +#endif Table* t = get_table(hTable); if (!t || pulLen == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); std::uint16_t idx = 0; @@ -3237,6 +3552,12 @@ UNSIGNED32 AdsIsRecordDeleted(ADSHANDLE hTable, UNSIGNED16* pbDeleted) { *pbDeleted = r.value() ? 1 : 0; return ok(); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + *pbDeleted = st->current_deleted ? 1 : 0; + return ok(); + } +#endif Table* t = get_table(hTable); if (!t) return fail(openads::AE_INTERNAL_ERROR, ""); *pbDeleted = t->is_deleted() ? 1 : 0; @@ -3923,6 +4244,38 @@ UNSIGNED32 AdsOpenIndex(ADSHANDLE hTable, UNSIGNED8* pucName, if (ahIndex == nullptr) { return fail(openads::AE_INTERNAL_ERROR, "null out"); } +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + if (pu16ArrayLen != nullptr && *pu16ArrayLen < 1) { + return fail(openads::AE_INTERNAL_ERROR, "index array too small"); + } + std::string tag = openads::abi::to_internal(pucName, 0); + if (tag.empty()) { + return fail(openads::AE_INTERNAL_ERROR, "empty index tag"); + } + const auto dot = tag.find_last_of("./\\"); + if (dot != std::string::npos) { + tag = tag.substr(dot + 1); + } + const auto dot2 = tag.find('.'); + if (dot2 != std::string::npos) { + tag = tag.substr(0, dot2); + } + auto si = std::make_unique(); + si->parent = st; + si->column = tag; + auto& s = state(); + std::lock_guard lk(s.mu); + Handle gh = s.registry.register_object( + HandleKind::MariaIndex, si.get()); + ahIndex[0] = gh; + if (pu16ArrayLen != nullptr) { + *pu16ArrayLen = 1; + } + maria_indexes_map().emplace(gh, std::move(si)); + return ok(); + } +#endif if (auto* rt = get_remote_table(hTable)) { std::string path = openads::abi::to_internal(pucName, 0); auto r = rt->conn->open_index(rt->id, path); @@ -4078,6 +4431,16 @@ UNSIGNED32 AdsOpenIndex(ADSHANDLE hTable, UNSIGNED8* pucName, } UNSIGNED32 AdsCloseIndex(ADSHANDLE hIndex) { +#if defined(OPENADS_WITH_MARIADB) + if (auto* si = get_maria_index(hIndex)) { + (void)si; + auto& s = state(); + std::lock_guard lk(s.mu); + maria_indexes_map().erase(hIndex); + s.registry.release(hIndex); + return ok(); + } +#endif if (auto* ri = get_remote_index(hIndex)) { auto r = ri->conn->close_index(ri->id); if (!r) return fail(r.error()); @@ -6321,6 +6684,12 @@ UNSIGNED32 AdsSetIndexDirection(ADSHANDLE hIndex, UNSIGNED16 usDir) { // engine set inside seek_key. UNSIGNED32 AdsIsFound(ADSHANDLE hTable, UNSIGNED16* pbFound) { if (pbFound == nullptr) return fail(openads::AE_INTERNAL_ERROR, ""); +#if defined(OPENADS_WITH_MARIADB) + if (auto* st = get_maria_table(hTable)) { + *pbFound = st->last_seek_found ? 1 : 0; + return ok(); + } +#endif if (auto* rt = get_remote_table(hTable)) { auto r = rt->conn->is_found(rt->id); if (!r) return fail(r.error()); @@ -6339,6 +6708,24 @@ UNSIGNED32 AdsSeek(ADSHANDLE hIndex, UNSIGNED16 u16KeyType, UNSIGNED16 u16SeekType, UNSIGNED16* pbFound) { +#if defined(OPENADS_WITH_MARIADB) + if (auto* si = get_maria_index(hIndex)) { + if (si->parent == nullptr || si->parent->conn == nullptr) { + return fail(openads::AE_INTERNAL_ERROR, "mariadb index orphan"); + } + std::string key(reinterpret_cast(pucKey), u16KeyLen); + const bool soft = (u16SeekType & 1u) != 0; + si->parent->row_valid = false; + auto r = si->parent->conn->seek_index( + si->parent, si->column, key, soft, /*last=*/false); + if (!r) return fail(r.error()); + const bool found = r.value(); + si->last_seek_found = found; + if (pbFound) *pbFound = found ? 1 : 0; + (void)u16KeyType; + return ok(); + } +#endif if (auto* ri = get_remote_index(hIndex)) { std::string key(reinterpret_cast(pucKey), u16KeyLen); @@ -6438,6 +6825,23 @@ UNSIGNED32 AdsSeekLast(ADSHANDLE hIndex, UNSIGNED16 u16KeyLen, UNSIGNED16 u16KeyType, UNSIGNED16* pbFound) { +#if defined(OPENADS_WITH_MARIADB) + if (auto* si = get_maria_index(hIndex)) { + if (si->parent == nullptr || si->parent->conn == nullptr) { + return fail(openads::AE_INTERNAL_ERROR, "mariadb index orphan"); + } + std::string key(reinterpret_cast(pucKey), u16KeyLen); + si->parent->row_valid = false; + auto r = si->parent->conn->seek_index( + si->parent, si->column, key, /*soft=*/false, /*last=*/true); + if (!r) return fail(r.error()); + const bool found = r.value(); + si->last_seek_found = found; + if (pbFound) *pbFound = found ? 1 : 0; + (void)u16KeyType; + return ok(); + } +#endif if (auto* ri = get_remote_index(hIndex)) { std::string key(reinterpret_cast(pucKey), u16KeyLen); diff --git a/src/session/handle_registry.h b/src/session/handle_registry.h index 1d585538..41547741 100644 --- a/src/session/handle_registry.h +++ b/src/session/handle_registry.h @@ -20,7 +20,11 @@ enum class HandleKind { // M12.16 — remote (TCP) index handle. Wraps a server-side // index id; ABI calls (AdsSeek, AdsCloseIndex, …) route the // hIndex through the wire instead of a local IIndex. - RemoteIndex = 8 + RemoteIndex = 8, + // Optional SQL backend (mariadb:// …). + MariaConnection = 9, + MariaTable = 10, + MariaIndex = 11 }; using Handle = std::uint64_t; diff --git a/src/sql_backend/maria_backend.cpp b/src/sql_backend/maria_backend.cpp new file mode 100644 index 00000000..ff9c9fa8 --- /dev/null +++ b/src/sql_backend/maria_backend.cpp @@ -0,0 +1,107 @@ +#include "sql_backend/maria_backend.h" + +#include "openads/ace.h" + +#include +#include + +#if defined(OPENADS_WITH_MARIADB) +#include +#endif + +namespace openads::sql_backend { + +MariaTable::FieldDesc map_maria_column(const char* name, + const char* data_type, + bool nullable, + int char_max_len, + int numeric_precision, + int numeric_scale) { + MariaTable::FieldDesc fd; + fd.name = name ? name : ""; + fd.nullable = nullable; + + std::string dt = data_type ? data_type : ""; + for (auto& c : dt) { + c = static_cast(std::tolower(static_cast(c))); + } + + if (dt.find("int") != std::string::npos || + dt == "tinyint" || dt == "smallint" || dt == "mediumint" || + dt == "bigint") { + fd.type = ADS_INTEGER; + fd.length = 4; + fd.decimals = 0; + } else if (dt.find("double") != std::string::npos || + dt.find("float") != std::string::npos || + dt.find("decimal") != std::string::npos || + dt.find("numeric") != std::string::npos) { + fd.type = ADS_DOUBLE; + fd.length = 8; + fd.decimals = numeric_scale > 0 + ? static_cast(numeric_scale) + : 6; + (void)numeric_precision; + } else if (dt.find("blob") != std::string::npos || + dt.find("binary") != std::string::npos) { + fd.type = ADS_BINARY; + fd.length = 10; + fd.decimals = 0; + } else { + fd.type = ADS_STRING; + fd.length = char_max_len > 0 + ? static_cast(char_max_len) + : 64; + fd.decimals = 0; + } + return fd; +} + +#if defined(OPENADS_WITH_MARIADB) + +std::string format_maria_value(char** row, unsigned int col, bool& is_null) { + is_null = false; + if (row == nullptr || row[col] == nullptr) { + is_null = true; + return {}; + } + return row[col]; +} + +util::Error maria_error(const char* context, const char* msg) { + return util::Error{5001, 0, + std::string(context) + ": " + (msg ? msg : ""), + ""}; +} + +#else + +std::string format_maria_value(char**, unsigned int, bool& is_null) { + is_null = true; + return {}; +} + +util::Error maria_error(const char* context, const char* msg) { + (void)context; + (void)msg; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +} + +#endif + +std::size_t field_index_ci(const MariaTable& tbl, const std::string& name) { + std::string want = name; + for (auto& c : want) { + c = static_cast(std::toupper(static_cast(c))); + } + for (std::size_t i = 0; i < tbl.fields.size(); ++i) { + std::string have = tbl.fields[i].name; + for (auto& c : have) { + c = static_cast(std::toupper(static_cast(c))); + } + if (have == want) return i; + } + return static_cast(-1); +} + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_backend.h b/src/sql_backend/maria_backend.h new file mode 100644 index 00000000..629a1cdf --- /dev/null +++ b/src/sql_backend/maria_backend.h @@ -0,0 +1,21 @@ +#pragma once + +#include "sql_backend/maria_table.h" +#include "util/result.h" + +namespace openads::sql_backend { + +MariaTable::FieldDesc map_maria_column(const char* name, + const char* data_type, + bool nullable, + int char_max_len, + int numeric_precision, + int numeric_scale); + +std::string format_maria_value(char** row, unsigned int col, bool& is_null); + +util::Error maria_error(const char* context, const char* msg); + +std::size_t field_index_ci(const MariaTable& tbl, const std::string& name); + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_connection.cpp b/src/sql_backend/maria_connection.cpp new file mode 100644 index 00000000..a8544ede --- /dev/null +++ b/src/sql_backend/maria_connection.cpp @@ -0,0 +1,601 @@ +#include "sql_backend/maria_connection.h" + +#include "sql_backend/maria_backend.h" +#include "sql_backend/sql_common.h" + +#include +#include + +#if defined(OPENADS_WITH_MARIADB) +#include +#endif + +namespace openads::sql_backend { + +namespace { + +#if defined(OPENADS_WITH_MARIADB) + +std::string quote_ident(const std::string& name) { + return '`' + name + '`'; +} + +std::string escape_literal(MYSQL* conn, const std::string& value) { + std::string out; + out.resize(value.size() * 2 + 1); + const unsigned long len = mysql_real_escape_string( + conn, out.data(), value.c_str(), + static_cast(value.size())); + out.resize(len); + return '\'' + out + '\''; +} + +std::string pk_where_clause(MYSQL* conn, const MariaTable& tbl, + const MariaTable::PkRow& pk) { + std::string sql; + for (std::size_t i = 0; i < tbl.pk_columns.size(); ++i) { + if (i > 0) sql += " AND "; + sql += quote_ident(tbl.pk_columns[i]) + " = " + + escape_literal(conn, pk.values[i]); + } + return sql; +} + +util::Result load_current_row(MYSQL* conn, MariaTable* tbl); + +util::Result> +discover_pk_columns(MYSQL* conn, const std::string& table_name) { + const std::string sql = + "SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE " + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + + table_name + + "' AND CONSTRAINT_NAME = 'PRIMARY' " + "ORDER BY ORDINAL_POSITION"; + if (mysql_query(conn, sql.c_str()) != 0) { + return maria_error("pk discovery", mysql_error(conn)); + } + MYSQL_RES* res = mysql_store_result(conn); + if (res == nullptr) { + return maria_error("pk discovery", mysql_error(conn)); + } + std::vector cols; + MYSQL_ROW row; + while ((row = mysql_fetch_row(res)) != nullptr) { + if (row[0] != nullptr) cols.emplace_back(row[0]); + } + mysql_free_result(res); + if (cols.empty()) { + return util::Error{5001, 0, "table has no primary key", table_name}; + } + return cols; +} + +util::Result> +describe_table_impl(MYSQL* conn, MariaTable* tbl) { + if (!is_safe_identifier(tbl->name)) { + return util::Error{5001, 0, "invalid table name", tbl->name}; + } + const std::string sql = + "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, " + "CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE " + "FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + + tbl->name + "' ORDER BY ORDINAL_POSITION"; + if (mysql_query(conn, sql.c_str()) != 0) { + return maria_error("describe_table", mysql_error(conn)); + } + MYSQL_RES* res = mysql_store_result(conn); + if (res == nullptr) { + return maria_error("describe_table", mysql_error(conn)); + } + const unsigned int rows = mysql_num_rows(res); + if (rows == 0) { + mysql_free_result(res); + return util::Error{5001, 0, "table not found or has no columns", + tbl->name}; + } + std::vector out; + out.reserve(rows); + MYSQL_ROW row; + while ((row = mysql_fetch_row(res)) != nullptr) { + const bool nullable = row[2] != nullptr && row[2][0] == 'Y'; + out.push_back(map_maria_column( + row[0], row[1], nullable, + row[3] ? std::atoi(row[3]) : 0, + row[4] ? std::atoi(row[4]) : 0, + row[5] ? std::atoi(row[5]) : 0)); + } + mysql_free_result(res); + tbl->fields = out; + tbl->fields_cached = true; + return out; +} + +util::Result position_at_pk(MYSQL* conn, MariaTable* tbl, + const MariaTable::PkRow& pk) { + if (tbl == nullptr || conn == nullptr) { + return util::Error{5001, 0, "invalid mariadb table state", ""}; + } + auto it = std::find_if(tbl->pk_snapshot.begin(), tbl->pk_snapshot.end(), + [&](const MariaTable::PkRow& row) { + return row.values == pk.values; + }); + if (it == tbl->pk_snapshot.end()) { + tbl->positioned = false; + tbl->row_valid = false; + return util::Result{}; + } + tbl->pos = static_cast( + std::distance(tbl->pk_snapshot.begin(), it)); + tbl->positioned = true; + tbl->current_recno = static_cast(tbl->pos + 1); + return load_current_row(conn, tbl); +} + +util::Result load_current_row(MYSQL* conn, MariaTable* tbl) { + if (tbl == nullptr || conn == nullptr) { + return util::Error{5001, 0, "invalid mariadb table state", ""}; + } + if (!tbl->positioned) { + tbl->row_valid = false; + tbl->current_row.clear(); + tbl->current_nulls.clear(); + return util::Result{}; + } + if (!tbl->fields_cached) { + auto d = describe_table_impl(conn, tbl); + if (!d) return d.error(); + } + + const std::string sql = + "SELECT * FROM " + quote_ident(tbl->name) + " WHERE " + + pk_where_clause(conn, *tbl, tbl->pk_snapshot[tbl->pos]); + if (mysql_query(conn, sql.c_str()) != 0) { + return maria_error("load row", mysql_error(conn)); + } + MYSQL_RES* res = mysql_store_result(conn); + if (res == nullptr) { + return maria_error("load row", mysql_error(conn)); + } + + tbl->current_row.clear(); + tbl->current_nulls.clear(); + tbl->row_valid = false; + + MYSQL_ROW row = mysql_fetch_row(res); + if (row != nullptr) { + const unsigned int cols = mysql_num_fields(res); + tbl->current_row.resize(cols); + tbl->current_nulls.resize(cols); + for (unsigned int c = 0; c < cols; ++c) { + bool is_null = false; + tbl->current_row[c] = format_maria_value(row, c, is_null); + tbl->current_nulls[c] = is_null; + } + tbl->row_valid = true; + } else { + tbl->positioned = false; + } + mysql_free_result(res); + return util::Result{}; +} + +std::string pk_select_list(const MariaTable& tbl) { + std::string out; + for (std::size_t i = 0; i < tbl.pk_columns.size(); ++i) { + if (i > 0) out += ", "; + out += quote_ident(tbl.pk_columns[i]); + } + return out; +} + +std::string pk_order_by(const MariaTable& tbl) { + return pk_select_list(tbl); +} + +#endif + +} // namespace + +struct MariaConnection::Impl { +#if defined(OPENADS_WITH_MARIADB) + MYSQL* conn = nullptr; +#endif +}; + +MariaConnection::MariaConnection() = default; +MariaConnection::~MariaConnection() { disconnect(); } + +MariaConnection::MariaConnection(MariaConnection&& other) noexcept + : impl_(std::move(other.impl_)) {} + +MariaConnection& MariaConnection::operator=(MariaConnection&& other) noexcept { + if (this != &other) { + disconnect(); + impl_ = std::move(other.impl_); + } + return *this; +} + +util::Result MariaConnection::open(const MariaUri& uri) { +#if defined(OPENADS_WITH_MARIADB) + MariaConnection conn; + conn.impl_ = std::make_unique(); + + MYSQL* raw = mysql_init(nullptr); + if (raw == nullptr) { + return util::Error{5001, 0, "mysql_init failed", ""}; + } + + const char* host = uri.host.empty() ? "127.0.0.1" : uri.host.c_str(); + const char* user = uri.user.empty() ? nullptr : uri.user.c_str(); + const char* pass = uri.password.empty() ? nullptr : uri.password.c_str(); + const char* db = uri.database.empty() ? nullptr : uri.database.c_str(); + + if (mysql_real_connect(raw, host, user, pass, db, uri.port, + nullptr, 0) == nullptr) { + util::Error e = maria_error("connect", mysql_error(raw)); + mysql_close(raw); + return e; + } + conn.impl_->conn = raw; + return std::move(conn); +#else + (void)uri; + return util::Error{5004, 0, + "mariadb backend requires OPENADS_WITH_MARIADB=ON", + ""}; +#endif +} + +void MariaConnection::disconnect() noexcept { +#if defined(OPENADS_WITH_MARIADB) + if (impl_ && impl_->conn) { + mysql_close(impl_->conn); + impl_->conn = nullptr; + } +#endif + impl_.reset(); +} + +bool MariaConnection::valid() const noexcept { +#if defined(OPENADS_WITH_MARIADB) + return impl_ && impl_->conn != nullptr; +#else + return false; +#endif +} + +util::Result> +MariaConnection::open_table(const std::string& table_name) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid()) { + return util::Error{5001, 0, "mariadb connection not open", ""}; + } + if (!is_safe_identifier(table_name)) { + return util::Error{5001, 0, "invalid table name", table_name}; + } + + auto tbl = std::make_unique(); + tbl->conn = this; + tbl->name = table_name; + + auto pk = discover_pk_columns(impl_->conn, table_name); + if (!pk) return pk.error(); + tbl->pk_columns = std::move(pk).value(); + + const std::string sql = + "SELECT " + pk_select_list(*tbl) + " FROM " + + quote_ident(table_name) + " ORDER BY " + pk_order_by(*tbl); + if (mysql_query(impl_->conn, sql.c_str()) != 0) { + return maria_error("pk snapshot", mysql_error(impl_->conn)); + } + MYSQL_RES* res = mysql_store_result(impl_->conn); + if (res == nullptr) { + return maria_error("pk snapshot", mysql_error(impl_->conn)); + } + + const unsigned int pk_cols = static_cast(tbl->pk_columns.size()); + MYSQL_ROW row; + while ((row = mysql_fetch_row(res)) != nullptr) { + MariaTable::PkRow pk_row; + pk_row.values.resize(pk_cols); + for (unsigned int c = 0; c < pk_cols; ++c) { + bool is_null = false; + pk_row.values[c] = format_maria_value(row, c, is_null); + if (is_null) pk_row.values[c].clear(); + } + tbl->pk_snapshot.push_back(std::move(pk_row)); + } + mysql_free_result(res); + + tbl->cached_rec_count = static_cast(tbl->pk_snapshot.size()); + tbl->rec_count_cached = true; + tbl->positioned = false; + tbl->row_valid = false; + tbl->current_recno = 0; + tbl->current_deleted = false; + tbl->pos = 0; + + if (auto d = describe_table_impl(impl_->conn, tbl.get()); !d) { + return d.error(); + } + return tbl; +#else + (void)table_name; + return util::Error{5004, 0, + "mariadb backend requires OPENADS_WITH_MARIADB=ON", + ""}; +#endif +} + +util::Result MariaConnection::goto_top(MariaTable* tbl) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb goto_top", ""}; + } + if (tbl->pk_snapshot.empty()) { + tbl->positioned = false; + tbl->row_valid = false; + tbl->current_recno = 0; + tbl->pos = 0; + return util::Result{}; + } + tbl->pos = 0; + tbl->positioned = true; + tbl->current_recno = 1; + return load_current_row(impl_->conn, tbl); +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::goto_bottom(MariaTable* tbl) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb goto_bottom", ""}; + } + if (tbl->pk_snapshot.empty()) { + tbl->positioned = false; + tbl->row_valid = false; + tbl->current_recno = 0; + tbl->pos = 0; + return util::Result{}; + } + tbl->pos = tbl->pk_snapshot.size() - 1; + tbl->positioned = true; + tbl->current_recno = static_cast(tbl->pos + 1); + return load_current_row(impl_->conn, tbl); +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::skip(MariaTable* tbl, std::int32_t step) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb skip", ""}; + } + if (step == 0) return util::Result{}; + if (tbl->pk_snapshot.empty()) { + tbl->positioned = false; + tbl->row_valid = false; + tbl->pos = 0; + return util::Result{}; + } + + std::int64_t next = 0; + if (!tbl->positioned) { + if (tbl->pos == 0) { + if (step > 0) next = step - 1; + else return util::Error{5026, 0, "bof", ""}; + } else { + if (step < 0) { + next = static_cast(tbl->pos) + step; + } else { + return util::Result{}; + } + } + } else { + next = static_cast(tbl->pos) + step; + } + + if (next < 0) { + tbl->positioned = false; + tbl->row_valid = false; + tbl->pos = 0; + return util::Error{5026, 0, "bof", ""}; + } + if (static_cast(next) >= tbl->pk_snapshot.size()) { + tbl->positioned = false; + tbl->row_valid = false; + tbl->pos = tbl->pk_snapshot.size(); + return util::Result{}; + } + + tbl->pos = static_cast(next); + tbl->positioned = true; + tbl->current_recno = static_cast(tbl->pos + 1); + return load_current_row(impl_->conn, tbl); +#else + (void)tbl; + (void)step; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::at_eof(MariaTable* tbl) const { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb at_eof", ""}; + } + if (tbl->pk_snapshot.empty()) return true; + if (!tbl->positioned && tbl->pos >= tbl->pk_snapshot.size()) return true; + return false; +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::at_bof(MariaTable* tbl) const { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb at_bof", ""}; + } + if (tbl->pk_snapshot.empty()) return true; + return !tbl->positioned && tbl->pos == 0; +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::record_count(MariaTable* tbl) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb record_count", ""}; + } + if (tbl->rec_count_cached) return tbl->cached_rec_count; + tbl->cached_rec_count = static_cast(tbl->pk_snapshot.size()); + tbl->rec_count_cached = true; + return tbl->cached_rec_count; +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result> +MariaConnection::describe_table(MariaTable* tbl) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb describe_table", ""}; + } + if (tbl->fields_cached) return tbl->fields; + return describe_table_impl(impl_->conn, tbl); +#else + (void)tbl; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::read_field( + MariaTable* tbl, const std::string& field_name, + std::string& buf, bool& is_null) const { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb read_field", ""}; + } + if (!tbl->fields_cached) { + return util::Error{5001, 0, "schema not cached", ""}; + } + if (!tbl->row_valid) { + return util::Error{5026, 0, "no current record", ""}; + } + + const std::size_t idx = field_index_ci(*tbl, field_name); + if (idx == static_cast(-1)) { + return util::Error{5063, 0, "column not found", field_name}; + } + if (idx >= tbl->current_row.size()) { + return util::Error{5001, 0, "row cache mismatch", ""}; + } + is_null = tbl->current_nulls[idx]; + buf = tbl->current_row[idx]; + return util::Result{}; +#else + (void)tbl; + (void)field_name; + (void)buf; + (void)is_null; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +util::Result MariaConnection::seek_index( + MariaTable* tbl, const std::string& column, const std::string& key, + bool soft, bool last_key) { +#if defined(OPENADS_WITH_MARIADB) + if (!valid() || tbl == nullptr) { + return util::Error{5001, 0, "invalid mariadb seek", ""}; + } + if (!is_safe_identifier(column)) { + return util::Error{5001, 0, "invalid seek column", column}; + } + if (!tbl->fields_cached) { + if (auto d = describe_table_impl(impl_->conn, tbl); !d) return d.error(); + } + if (field_index_ci(*tbl, column) == static_cast(-1)) { + return util::Error{5063, 0, "seek column not found", column}; + } + + const std::string pk_cols = pk_select_list(*tbl); + const std::string esc_key = escape_literal(impl_->conn, key); + const std::string qcol = quote_ident(column); + + std::string sql; + if (last_key) { + sql = soft + ? "SELECT " + pk_cols + " FROM " + quote_ident(tbl->name) + + " WHERE " + qcol + " <= " + esc_key + + " ORDER BY " + qcol + " DESC LIMIT 1" + : "SELECT " + pk_cols + " FROM " + quote_ident(tbl->name) + + " WHERE " + qcol + " = " + esc_key + + " ORDER BY " + qcol + " DESC LIMIT 1"; + } else { + sql = soft + ? "SELECT " + pk_cols + " FROM " + quote_ident(tbl->name) + + " WHERE " + qcol + " >= " + esc_key + + " ORDER BY " + qcol + " ASC LIMIT 1" + : "SELECT " + pk_cols + " FROM " + quote_ident(tbl->name) + + " WHERE " + qcol + " = " + esc_key + " LIMIT 1"; + } + + if (mysql_query(impl_->conn, sql.c_str()) != 0) { + return maria_error("seek", mysql_error(impl_->conn)); + } + MYSQL_RES* res = mysql_store_result(impl_->conn); + if (res == nullptr) { + return maria_error("seek", mysql_error(impl_->conn)); + } + + bool found = false; + MYSQL_ROW row = mysql_fetch_row(res); + if (row != nullptr) { + MariaTable::PkRow pk; + const unsigned int pk_cols_n = + static_cast(tbl->pk_columns.size()); + pk.values.resize(pk_cols_n); + for (unsigned int c = 0; c < pk_cols_n; ++c) { + bool is_null = false; + pk.values[c] = format_maria_value(row, c, is_null); + if (is_null) pk.values[c].clear(); + } + mysql_free_result(res); + if (auto p = position_at_pk(impl_->conn, tbl, pk); !p) { + return p.error(); + } + found = tbl->positioned && tbl->row_valid; + tbl->last_seek_found = found; + } else { + mysql_free_result(res); + tbl->positioned = false; + tbl->row_valid = false; + found = false; + tbl->last_seek_found = false; + } + return found; +#else + (void)tbl; + (void)column; + (void)key; + (void)soft; + (void)last_key; + return util::Error{5004, 0, "mariadb backend disabled", ""}; +#endif +} + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_connection.h b/src/sql_backend/maria_connection.h new file mode 100644 index 00000000..c464dc57 --- /dev/null +++ b/src/sql_backend/maria_connection.h @@ -0,0 +1,59 @@ +#pragma once + +#include "sql_backend/maria_table.h" +#include "sql_backend/maria_uri.h" +#include "util/result.h" + +#include +#include +#include + +namespace openads::sql_backend { + +class MariaConnection { +public: + MariaConnection(); + ~MariaConnection(); + + MariaConnection(MariaConnection&&) noexcept; + MariaConnection& operator=(MariaConnection&&) noexcept; + + MariaConnection(const MariaConnection&) = delete; + MariaConnection& operator=(const MariaConnection&) = delete; + + static util::Result open(const MariaUri& uri); + + void disconnect() noexcept; + bool valid() const noexcept; + + util::Result> + open_table(const std::string& table_name); + + util::Result goto_top(MariaTable* tbl); + util::Result goto_bottom(MariaTable* tbl); + util::Result skip(MariaTable* tbl, std::int32_t step); + + util::Result at_eof(MariaTable* tbl) const; + util::Result at_bof(MariaTable* tbl) const; + util::Result record_count(MariaTable* tbl); + + util::Result> + describe_table(MariaTable* tbl); + + util::Result read_field(MariaTable* tbl, + const std::string& field_name, + std::string& buf, + bool& is_null) const; + + util::Result seek_index(MariaTable* tbl, + const std::string& column, + const std::string& key, + bool soft, + bool last_key); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_index.h b/src/sql_backend/maria_index.h new file mode 100644 index 00000000..65377f18 --- /dev/null +++ b/src/sql_backend/maria_index.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace openads::sql_backend { + +struct MariaTable; + +struct MariaIndex { + MariaTable* parent = nullptr; + std::string column; + bool last_seek_found = false; +}; + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_table.h b/src/sql_backend/maria_table.h new file mode 100644 index 00000000..4327cc8d --- /dev/null +++ b/src/sql_backend/maria_table.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace openads::sql_backend { + +class MariaConnection; + +struct MariaTable { + MariaConnection* conn = nullptr; + std::string name; + + struct FieldDesc { + std::string name; + std::uint16_t type = 0; + std::uint32_t length = 0; + std::uint16_t decimals = 0; + bool nullable = true; + }; + + std::vector fields; + bool fields_cached = false; + + std::vector current_row; + std::vector current_nulls; + bool row_valid = false; + + std::uint32_t current_recno = 0; + bool current_deleted = false; + std::uint32_t cached_rec_count = 0; + bool rec_count_cached = false; + + std::vector pk_columns; + struct PkRow { + std::vector values; + }; + std::vector pk_snapshot; + std::size_t pos = 0; + bool positioned = false; + bool last_seek_found = false; +}; + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_uri.cpp b/src/sql_backend/maria_uri.cpp new file mode 100644 index 00000000..c6db9066 --- /dev/null +++ b/src/sql_backend/maria_uri.cpp @@ -0,0 +1,94 @@ +#include "sql_backend/maria_uri.h" + +#include + +namespace openads::sql_backend { + +namespace { + +bool parse_authority(const std::string& auth, MariaUri& out) { + std::string userinfo; + std::string hostport; + const auto at = auth.find('@'); + if (at == std::string::npos) { + hostport = auth; + } else { + userinfo = auth.substr(0, at); + hostport = auth.substr(at + 1); + } + if (!userinfo.empty()) { + const auto colon = userinfo.find(':'); + if (colon == std::string::npos) { + out.user = userinfo; + } else { + out.user = userinfo.substr(0, colon); + out.password = userinfo.substr(colon + 1); + } + } + if (hostport.empty()) return false; + if (hostport.front() == '[') { + const auto close = hostport.find(']'); + if (close == std::string::npos) return false; + out.host = hostport.substr(1, close - 1); + if (close + 1 < hostport.size() && hostport[close + 1] == ':') { + out.port = static_cast( + std::strtoul(hostport.c_str() + close + 2, nullptr, 10)); + } + } else { + const auto colon = hostport.rfind(':'); + if (colon != std::string::npos) { + const std::string port_str = hostport.substr(colon + 1); + bool all_digits = !port_str.empty(); + for (char c : port_str) { + if (c < '0' || c > '9') { + all_digits = false; + break; + } + } + if (all_digits) { + out.host = hostport.substr(0, colon); + out.port = static_cast( + std::strtoul(port_str.c_str(), nullptr, 10)); + } else { + out.host = hostport; + } + } else { + out.host = hostport; + } + } + return !out.host.empty(); +} + +} // namespace + +bool parse_maria_uri(const std::string& uri, MariaUri& out) { + static constexpr const char* kMaria = "mariadb://"; + static constexpr const char* kMysql = "mysql://"; + const auto mlen = std::char_traits::length(kMaria); + const auto mylen = std::char_traits::length(kMysql); + + std::string rest; + if (uri.size() >= mlen && uri.compare(0, mlen, kMaria) == 0) { + rest = uri.substr(mlen); + } else if (uri.size() >= mylen && uri.compare(0, mylen, kMysql) == 0) { + rest = uri.substr(mylen); + } else { + return false; + } + + out = MariaUri{}; + const auto slash = rest.find('/'); + std::string authority = rest; + if (slash != std::string::npos) { + authority = rest.substr(0, slash); + out.database = rest.substr(slash + 1); + const auto q = out.database.find('?'); + if (q != std::string::npos) { + out.database = out.database.substr(0, q); + } + } + if (!parse_authority(authority, out)) return false; + return true; +} + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/maria_uri.h b/src/sql_backend/maria_uri.h new file mode 100644 index 00000000..37549dd1 --- /dev/null +++ b/src/sql_backend/maria_uri.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace openads::sql_backend { + +struct MariaUri { + std::string host; + std::string user; + std::string password; + std::string database; + std::uint16_t port = 3306; +}; + +bool parse_maria_uri(const std::string& uri, MariaUri& out); + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/sql_common.cpp b/src/sql_backend/sql_common.cpp new file mode 100644 index 00000000..a9d44f81 --- /dev/null +++ b/src/sql_backend/sql_common.cpp @@ -0,0 +1,17 @@ +#include "sql_backend/sql_common.h" + +namespace openads::sql_backend { + +bool is_safe_identifier(const std::string& name) { + if (name.empty()) return false; + for (char c : name) { + const bool ok = (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_'; + if (!ok) return false; + } + return true; +} + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/src/sql_backend/sql_common.h b/src/sql_backend/sql_common.h new file mode 100644 index 00000000..55edb27d --- /dev/null +++ b/src/sql_backend/sql_common.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace openads::sql_backend { + +bool is_safe_identifier(const std::string& name); + +} // namespace openads::sql_backend \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 21d33f6f..db8a739d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -142,6 +142,13 @@ add_executable(openads_unit_tests unit/abi_sql_dd_sql_test.cpp ) +if(OPENADS_WITH_MARIADB) + target_sources(openads_unit_tests PRIVATE + unit/abi_plus_mariadb_read_test.cpp + unit/abi_plus_mariadb_seek_test.cpp + ) +endif() + # M11.4 — side DLL with stored procedures used by abi_aep_test. add_library(openads_test_aep_proc SHARED fixtures/test_aep_proc.cpp diff --git a/tests/unit/abi_plus_mariadb_read_test.cpp b/tests/unit/abi_plus_mariadb_read_test.cpp new file mode 100644 index 00000000..59a2ede9 --- /dev/null +++ b/tests/unit/abi_plus_mariadb_read_test.cpp @@ -0,0 +1,135 @@ +#include "doctest.h" +#include "openads/ace.h" +#include "openads/error.h" + +#include +#include +#include +#include +#include + +#if defined(OPENADS_WITH_MARIADB) +#include "sql_backend/maria_uri.h" +#include +#endif + +#if defined(OPENADS_WITH_MARIADB) + +namespace { + +constexpr const char* kDefaultMariaUri = + "mariadb://root@127.0.0.1:3306/test"; + +const char* test_maria_uri() { + const char* uri_env = std::getenv("OPENADS_TEST_MARIADB_URI"); + if (uri_env != nullptr && uri_env[0] != '\0') { + return uri_env; + } + return kDefaultMariaUri; +} + +void seed_fixture(MYSQL* conn) { + auto exec = [&](const char* sql) { + if (mysql_query(conn, sql) != 0) { + const char* msg = mysql_error(conn); + std::string detail = "seed failed"; + if (msg != nullptr && msg[0] != '\0') { + detail += ": "; + detail += msg; + } + FAIL(detail); + } + }; + exec("DROP TABLE IF EXISTS clientes"); + exec("CREATE TABLE clientes (" + "id INT PRIMARY KEY, nome VARCHAR(64), saldo DOUBLE)"); + exec("INSERT INTO clientes (id, nome, saldo) VALUES " + "(1, 'Ana', 10.5), (2, 'Bob', NULL), (3, 'Cid', 0.0)"); +} + +std::string field_str(ADSHANDLE hTable, const char* name) { + UNSIGNED8 fld[32]; + std::memcpy(fld, name, std::strlen(name) + 1); + UNSIGNED8 buf[128] = {0}; + UNSIGNED32 cap = sizeof(buf); + REQUIRE(AdsGetField(hTable, fld, buf, &cap, 0) == 0); + return std::string(reinterpret_cast(buf), cap); +} + +} // namespace + +TEST_CASE("ABI: mariadb read-only AdsOpenTable navigation") { + const char* uri_cstr = test_maria_uri(); + + MYSQL* seed = mysql_init(nullptr); + REQUIRE(seed != nullptr); + openads::sql_backend::MariaUri muri; + REQUIRE(openads::sql_backend::parse_maria_uri(uri_cstr, muri)); + const char* host = muri.host.empty() ? "127.0.0.1" : muri.host.c_str(); + const char* user = muri.user.empty() ? nullptr : muri.user.c_str(); + const char* pass = muri.password.empty() ? nullptr : muri.password.c_str(); + const char* db = muri.database.empty() ? nullptr : muri.database.c_str(); + REQUIRE(mysql_real_connect(seed, host, user, pass, db, muri.port, + nullptr, 0) != nullptr); + seed_fixture(seed); + mysql_close(seed); + + const std::string uri = uri_cstr; + std::vector srv(uri.size() + 1); + std::memcpy(srv.data(), uri.c_str(), uri.size() + 1); + + ADSHANDLE hConn = 0; + REQUIRE(AdsConnect60(srv.data(), ADS_LOCAL_SERVER, + nullptr, nullptr, 0, &hConn) == 0); + + UNSIGNED8 tbl_name[32] = "clientes"; + ADSHANDLE hTable = 0; + REQUIRE(AdsOpenTable(hConn, tbl_name, tbl_name, + ADS_DEFAULT, 0, 0, 0, ADS_READONLY, + &hTable) == 0); + + UNSIGNED16 nfields = 0; + REQUIRE(AdsGetNumFields(hTable, &nfields) == 0); + CHECK(nfields == 3); + + UNSIGNED32 count = 0; + REQUIRE(AdsGetRecordCount(hTable, 0, &count) == 0); + CHECK(count == 3); + + REQUIRE(AdsGotoTop(hTable) == 0); + + UNSIGNED16 bof = 1; + REQUIRE(AdsAtBOF(hTable, &bof) == 0); + CHECK(bof == 0); + + CHECK(field_str(hTable, "nome") == std::string(64, ' ').replace(0, 3, "Ana")); + + REQUIRE(AdsSkip(hTable, 1) == 0); + CHECK(field_str(hTable, "saldo").empty()); + + REQUIRE(AdsSkip(hTable, 1) == 0); + CHECK(field_str(hTable, "nome") == std::string(64, ' ').replace(0, 3, "Cid")); + + UNSIGNED16 eof = 0; + REQUIRE(AdsAtEOF(hTable, &eof) == 0); + CHECK(eof == 0); + + REQUIRE(AdsSkip(hTable, 1) == 0); + REQUIRE(AdsAtEOF(hTable, &eof) == 0); + CHECK(eof == 1); + + REQUIRE(AdsCloseTable(hTable) == 0); + REQUIRE(AdsDisconnect(hConn) == 0); +} + +#else + +TEST_CASE("ABI: mariadb backend disabled at compile time") { + UNSIGNED8 uri[] = "mariadb://127.0.0.1/none"; + ADSHANDLE hConn = 0; + const UNSIGNED32 rc = AdsConnect60(uri, ADS_LOCAL_SERVER, + nullptr, nullptr, 0, &hConn); + CHECK(rc == openads::AE_FUNCTION_NOT_AVAILABLE); +} + +#endif \ No newline at end of file diff --git a/tests/unit/abi_plus_mariadb_seek_test.cpp b/tests/unit/abi_plus_mariadb_seek_test.cpp new file mode 100644 index 00000000..c8fd11be --- /dev/null +++ b/tests/unit/abi_plus_mariadb_seek_test.cpp @@ -0,0 +1,114 @@ +#include "doctest.h" +#include "openads/ace.h" +#include "openads/error.h" + +#include +#include +#include +#include +#include + +#if defined(OPENADS_WITH_MARIADB) +#include "sql_backend/maria_uri.h" +#include +#endif + +#if defined(OPENADS_WITH_MARIADB) + +namespace { + +constexpr const char* kDefaultMariaUri = + "mariadb://root@127.0.0.1:3306/test"; + +const char* test_maria_uri() { + const char* uri_env = std::getenv("OPENADS_TEST_MARIADB_URI"); + if (uri_env != nullptr && uri_env[0] != '\0') { + return uri_env; + } + return kDefaultMariaUri; +} + +void seed_fixture(MYSQL* conn) { + auto exec = [&](const char* sql) { + if (mysql_query(conn, sql) != 0) { + FAIL("seed failed"); + } + }; + exec("DROP TABLE IF EXISTS clientes"); + exec("CREATE TABLE clientes (id INT PRIMARY KEY, nome VARCHAR(64))"); + exec("INSERT INTO clientes (id, nome) VALUES (1,'Ana'),(2,'Bob'),(3,'Cid')"); +} + +std::string field_str(ADSHANDLE hTable, const char* name) { + UNSIGNED8 fld[32]; + std::memcpy(fld, name, std::strlen(name) + 1); + UNSIGNED8 buf[128] = {0}; + UNSIGNED32 cap = sizeof(buf); + REQUIRE(AdsGetField(hTable, fld, buf, &cap, 0) == 0); + return std::string(reinterpret_cast(buf), cap); +} + +} // namespace + +TEST_CASE("ABI: mariadb AdsSeek on column index") { + const char* uri_cstr = test_maria_uri(); + + MYSQL* seed = mysql_init(nullptr); + REQUIRE(seed != nullptr); + openads::sql_backend::MariaUri muri; + REQUIRE(openads::sql_backend::parse_maria_uri(uri_cstr, muri)); + const char* host = muri.host.empty() ? "127.0.0.1" : muri.host.c_str(); + const char* user = muri.user.empty() ? nullptr : muri.user.c_str(); + const char* pass = muri.password.empty() ? nullptr : muri.password.c_str(); + const char* db = muri.database.empty() ? nullptr : muri.database.c_str(); + REQUIRE(mysql_real_connect(seed, host, user, pass, db, muri.port, + nullptr, 0) != nullptr); + seed_fixture(seed); + mysql_close(seed); + + const std::string uri = uri_cstr; + std::vector srv(uri.size() + 1); + std::memcpy(srv.data(), uri.c_str(), uri.size() + 1); + + ADSHANDLE hConn = 0; + REQUIRE(AdsConnect60(srv.data(), ADS_LOCAL_SERVER, + nullptr, nullptr, 0, &hConn) == 0); + + UNSIGNED8 tbl[32] = "clientes"; + ADSHANDLE hTable = 0; + REQUIRE(AdsOpenTable(hConn, tbl, tbl, ADS_DEFAULT, 0, 0, 0, + ADS_READONLY, &hTable) == 0); + + UNSIGNED8 tag[16] = "id"; + ADSHANDLE hIndex = 0; + UNSIGNED16 nIdx = 1; + REQUIRE(AdsOpenIndex(hTable, tag, &hIndex, &nIdx) == 0); + CHECK(nIdx == 1); + + const char key[] = "2"; + UNSIGNED16 found = 0; + REQUIRE(AdsSeek(hIndex, + reinterpret_cast(const_cast(key)), + static_cast(std::strlen(key)), + ADS_STRINGKEY, 0, &found) == 0); + CHECK(found == 1); + + UNSIGNED16 is_found = 0; + REQUIRE(AdsIsFound(hTable, &is_found) == 0); + CHECK(is_found == 1); + + CHECK(field_str(hTable, "nome").find("Bob") != std::string::npos); + + const char miss[] = "9"; + REQUIRE(AdsSeek(hIndex, + reinterpret_cast(const_cast(miss)), + static_cast(std::strlen(miss)), + ADS_STRINGKEY, 0, &found) == 0); + CHECK(found == 0); + + REQUIRE(AdsCloseIndex(hIndex) == 0); + REQUIRE(AdsCloseTable(hTable) == 0); + REQUIRE(AdsDisconnect(hConn) == 0); +} + +#endif \ No newline at end of file diff --git a/tools/scripts/build_msvc_x64_mariadb.bat b/tools/scripts/build_msvc_x64_mariadb.bat new file mode 100644 index 00000000..fdbab4c9 --- /dev/null +++ b/tools/scripts/build_msvc_x64_mariadb.bat @@ -0,0 +1,27 @@ +@echo off +REM MariaDB backend build with MSVC cl (no winlibs GCC runtime at run time). +if not defined OPENADS_TOOLCHAIN_ROOT ( + echo ERROR: set OPENADS_TOOLCHAIN_ROOT to the directory that holds the + echo msvc, winlibs and mariadb client dependency folders. + exit /b 1 +) +if not defined OPENADS_LIBMARIADB_INCLUDE ( + set "OPENADS_LIBMARIADB_INCLUDE=%OPENADS_TOOLCHAIN_ROOT%\mariadb\include" +) +if not defined OPENADS_LIBMARIADB_LIBRARY ( + set "OPENADS_LIBMARIADB_LIBRARY=%OPENADS_TOOLCHAIN_ROOT%\mariadb\lib\libmariadb64.lib" +) +call "%OPENADS_TOOLCHAIN_ROOT%\msvc\setup_x64.bat" +if exist "%OPENADS_TOOLCHAIN_ROOT%\winlibs-x86_64\bin" ( + set "PATH=%OPENADS_TOOLCHAIN_ROOT%\winlibs-x86_64\bin;%PATH%" +) +cd /d "%~dp0..\.." +cmake -S . -B build\maria-msvc -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl ^ + -DOPENADS_WITH_MARIADB=ON -DOPENADS_WITH_HTTP=OFF ^ + -DOPENADS_WARNINGS_AS_ERRORS=OFF ^ + -DOPENADS_LIBMARIADB_INCLUDE=%OPENADS_LIBMARIADB_INCLUDE% ^ + -DOPENADS_LIBMARIADB_LIBRARY=%OPENADS_LIBMARIADB_LIBRARY% +if errorlevel 1 exit /b 1 +cmake --build build\maria-msvc +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/tools/scripts/run_mariadb_tests.bat b/tools/scripts/run_mariadb_tests.bat new file mode 100644 index 00000000..b6eb9b18 --- /dev/null +++ b/tools/scripts/run_mariadb_tests.bat @@ -0,0 +1,30 @@ +@echo off +REM Run MariaDB ABI e2e tests from build\maria-msvc (or pass BUILD_DIR). +setlocal +set "ROOT=%~dp0..\.." +set "BUILD=%ROOT%\build\maria-msvc" +if not "%~1"=="" set "BUILD=%~1" + +if not exist "%BUILD%\tests\openads_unit_tests.exe" ( + echo ERROR: %BUILD%\tests\openads_unit_tests.exe not found. Build first. + exit /b 1 +) + +if not defined OPENADS_TOOLCHAIN_ROOT ( + echo ERROR: set OPENADS_TOOLCHAIN_ROOT to the directory that holds the + echo mariadb client and winlibs (for libmariadb PATH). + exit /b 1 +) + +set "PATH=%OPENADS_TOOLCHAIN_ROOT%\mariadb\bin;%PATH%" +if exist "%OPENADS_TOOLCHAIN_ROOT%\winlibs-x86_64\bin" ( + set "PATH=%OPENADS_TOOLCHAIN_ROOT%\winlibs-x86_64\bin;%PATH%" +) + +if not defined OPENADS_TEST_MARIADB_URI ( + set "OPENADS_TEST_MARIADB_URI=mariadb://root@127.0.0.1:3306/test" +) + +cd /d "%BUILD%" +tests\openads_unit_tests.exe --test-case=*mariadb* %* +exit /b %ERRORLEVEL% \ No newline at end of file