Skip to content

Commit c1429c9

Browse files
authored
Add initial MySQL/MariaDB driver support with thread-based async I/O and proper prepared-statement binding (#57)
1 parent fdea52a commit c1429c9

15 files changed

Lines changed: 1822 additions & 193 deletions

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
-DCMAKE_PREFIX_PATH="/opt/homebrew/opt/postgresql@16;C:\Program Files\PostgreSQL\16"
6767
-DENABLE_MAINTAINER_CFLAGS=${{ matrix.build_type == 'Debug' }}
6868
-DBUILD_SHARED_LIBS=ON
69+
-DDRIVER_MYSQL=${{ runner.os == 'Linux' }}
6970
7071
- name: Build project
7172
run: cmake --build ./build-shared
@@ -87,6 +88,7 @@ jobs:
8788
-DCMAKE_BUILD_TYPE=Debug
8889
-DCMAKE_PREFIX_PATH="/opt/homebrew/opt/postgresql@16;C:\Program Files\PostgreSQL\16"
8990
-DBUILD_SHARED_LIBS=OFF
91+
-DDRIVER_MYSQL=${{ runner.os == 'Linux' }}
9092
9193
- name: Build project
9294
run: cmake --build ./build-static

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@
3333

3434
build*/
3535
*.user
36+
CMakeFiles/

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ if (ASQL_DRIVER_POSTGRES)
6262
find_package(PostgreSQL REQUIRED)
6363
endif ()
6464

65+
option(DRIVER_MYSQL "Enable MySQL Driver" ON)
66+
if (DRIVER_MYSQL)
67+
find_package(MySQL REQUIRED)
68+
endif ()
69+
6570
option(ASQL_DRIVER_ODBC "Enable unixODBC Driver" ON)
6671
if (ASQL_DRIVER_ODBC)
6772
find_package(ODBC REQUIRED)

cmake/modules/FindMySQL.cmake

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# SPDX-FileCopyrightText: (C) 2025 Daniel Nicoletti <dantti12@gmail.com>
2+
# SPDX-License-Identifier: MIT
3+
4+
# FindMySQL
5+
# ---------
6+
# Finds the MySQL/MariaDB client library (libmysqlclient or libmariadb).
7+
#
8+
# Both MySQL 8.0+ and MariaDB Connector/C implement the same non-blocking API
9+
# (mysql_real_query_nonblocking, mysql_store_result_nonblocking, etc.) with a
10+
# compatible ABI. libmariadb-dev ships a mysqlclient.pc compatibility file, so
11+
# on most distributions only one of the two will be installed at a time.
12+
#
13+
# Imported Targets
14+
# ^^^^^^^^^^^^^^^^
15+
# MySQL::MySQL
16+
#
17+
# Result Variables
18+
# ^^^^^^^^^^^^^^^^
19+
# MySQL_FOUND
20+
# MySQL_INCLUDE_DIRS
21+
# MySQL_LIBRARIES
22+
# MySQL_VERSION
23+
24+
find_package(PkgConfig QUIET)
25+
if (PkgConfig_FOUND)
26+
# Prefer the MariaDB Connector/C pkg-config name; fall back to mysqlclient
27+
# (which MariaDB also provides as a compatibility alias).
28+
pkg_check_modules(PC_MySQL QUIET libmariadb)
29+
if (NOT PC_MySQL_FOUND)
30+
pkg_check_modules(PC_MySQL QUIET mysqlclient)
31+
endif ()
32+
endif ()
33+
34+
find_path(MySQL_INCLUDE_DIR
35+
NAMES mysql/mysql.h mysql.h
36+
HINTS
37+
${PC_MySQL_INCLUDEDIR}
38+
${PC_MySQL_INCLUDE_DIRS}
39+
PATH_SUFFIXES mysql mariadb
40+
)
41+
42+
find_library(MySQL_LIBRARY
43+
NAMES mariadb mysqlclient mysqlclient_r
44+
HINTS
45+
${PC_MySQL_LIBDIR}
46+
${PC_MySQL_LIBRARY_DIRS}
47+
)
48+
49+
if (PC_MySQL_VERSION)
50+
set(MySQL_VERSION ${PC_MySQL_VERSION})
51+
elseif (MySQL_INCLUDE_DIR)
52+
# MySQL stores version in mysql/mysql_version.h; MariaDB uses mariadb_version.h
53+
foreach(_version_header
54+
"${MySQL_INCLUDE_DIR}/mysql/mysql_version.h"
55+
"${MySQL_INCLUDE_DIR}/mariadb_version.h"
56+
"${MySQL_INCLUDE_DIR}/mysql_version.h")
57+
if (EXISTS "${_version_header}")
58+
file(STRINGS "${_version_header}" _mysql_version_line
59+
REGEX "^#define (MYSQL|MARIADB)_SERVER_VERSION[ \t]+\"[^\"]+\"")
60+
if (_mysql_version_line)
61+
string(REGEX REPLACE ".*\"([^\"]+)\".*" "\\1" MySQL_VERSION "${_mysql_version_line}")
62+
break()
63+
endif()
64+
endif()
65+
endforeach()
66+
endif ()
67+
68+
include(FindPackageHandleStandardArgs)
69+
find_package_handle_standard_args(MySQL
70+
REQUIRED_VARS MySQL_LIBRARY MySQL_INCLUDE_DIR
71+
VERSION_VAR MySQL_VERSION
72+
)
73+
74+
if (MySQL_FOUND AND NOT TARGET MySQL::MySQL)
75+
add_library(MySQL::MySQL UNKNOWN IMPORTED)
76+
set_target_properties(MySQL::MySQL PROPERTIES
77+
IMPORTED_LOCATION "${MySQL_LIBRARY}"
78+
INTERFACE_INCLUDE_DIRECTORIES "${MySQL_INCLUDE_DIR}"
79+
)
80+
endif ()
81+
82+
mark_as_advanced(MySQL_INCLUDE_DIR MySQL_LIBRARY)

demos/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ endif()
88
if (ASQL_DRIVER_SQLITE)
99
add_subdirectory(sqlite)
1010
endif()
11+
12+
if (DRIVER_MYSQL)
13+
add_subdirectory(mysql)
14+
endif()

demos/mysql/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-FileCopyrightText: (C) 2020 Daniel Nicoletti <dantti12@gmail.com>
2+
# SPDX-License-Identifier: MIT
3+
4+
add_executable(mysql mysql.cpp)
5+
target_link_libraries(mysql
6+
ASql::Core
7+
ASql::Mysql
8+
Qt::Core
9+
)

demos/mysql/mysql.cpp

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* SPDX-FileCopyrightText: (C) 2026 Daniel Nicoletti <dantti12@gmail.com>
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
#include "../../src/acache.h"
7+
#include "../../src/acoroexpected.h"
8+
#include "../../src/adatabase.h"
9+
#include "../../src/amigrations.h"
10+
#include "../../src/amysql.h"
11+
#include "../../src/apool.h"
12+
#include "../../src/apreparedquery.h"
13+
#include "../../src/aresult.h"
14+
#include "../../src/atransaction.h"
15+
16+
#include <thread>
17+
18+
#include <QCoreApplication>
19+
#include <QDateTime>
20+
#include <QElapsedTimer>
21+
#include <QJsonArray>
22+
#include <QJsonObject>
23+
#include <QLoggingCategory>
24+
#include <QScopeGuard>
25+
#include <QTimer>
26+
27+
using namespace ASql;
28+
using namespace Qt::StringLiterals;
29+
30+
int main(int argc, char *argv[])
31+
{
32+
QCoreApplication app(argc, argv);
33+
34+
APool::create(AMysql::factory(u"mysql://user:pass@localhost/test_db"_s));
35+
APool::setMaxIdleConnections(10);
36+
37+
if (true) {
38+
auto callTerminatorEarly = []() -> ACoroTerminator {
39+
auto _ = qScopeGuard([] { qDebug() << "coro exited"; });
40+
qDebug() << "coro started";
41+
42+
auto result = co_await APool::exec(u"SELECT error()"_s);
43+
if (result) {
44+
qDebug() << "coro result has value" << result->toJsonObject();
45+
} else {
46+
qDebug() << "coro result error" << result.error();
47+
}
48+
49+
result = co_await APool::exec(u"SELECT 123 num"_s);
50+
if (result) {
51+
qDebug() << "coro result has value" << result->toJsonObject();
52+
} else {
53+
qDebug() << "coro result error" << result.error();
54+
}
55+
56+
result = co_await APool::exec(u"SELECT 'a', 'b', 321"_s);
57+
if (result) {
58+
qDebug() << "coro result has value" << result->columnNames()
59+
<< result->toJsonObject();
60+
} else {
61+
qDebug() << "coro result error" << result.error();
62+
}
63+
64+
result = co_await APool::exec(u"SELECT ?"_s,
65+
{
66+
1,
67+
// true,
68+
// u"foo"_s,
69+
});
70+
if (result) {
71+
qDebug() << "coro result has value" << result->columnNames()
72+
<< result->toJsonObject();
73+
} else {
74+
qDebug() << "coro result error" << result.error();
75+
}
76+
77+
result =
78+
co_await APool::exec(u"SELECT *, RAND() FROM (VALUES (1), (2), (3), (4), (5))"_s);
79+
if (result) {
80+
qDebug() << "coro result has value" << result->columnNames()
81+
<< result->toJsonArrayObject();
82+
} else {
83+
qDebug() << "coro result error" << result.error();
84+
}
85+
86+
result = co_await APool::exec(u"SELECT * FROM foo"_s);
87+
if (result) {
88+
qDebug() << "coro result has value" << result->columnNames()
89+
<< result->toJsonArrayObject();
90+
} else {
91+
qDebug() << "coro result error" << result.error();
92+
}
93+
94+
result = co_await APool::exec(u"SELECT now()"_s);
95+
if (result) {
96+
qDebug() << "coro result has value" << result->columnNames()
97+
<< result->toJsonObject();
98+
} else {
99+
qDebug() << "coro result error" << result.error();
100+
}
101+
102+
qApp->quit();
103+
};
104+
callTerminatorEarly();
105+
}
106+
107+
if (false) {
108+
auto callTerminatorEarly = []() -> ACoroTerminator {
109+
auto _ = qScopeGuard([] { qDebug() << "coro exited"; });
110+
qDebug() << "coro started prepared queries";
111+
112+
// TODO database returns a db handle that is not open yet.
113+
// because database() method calls open() and do not wait it.
114+
auto db = co_await APool::database();
115+
if (db) {
116+
qDebug() << "coro db isOpen" << db->isOpen();
117+
} else {
118+
qDebug() << "coro db error" << db.error();
119+
co_return;
120+
}
121+
122+
for (int i = 0; i < 3; ++i) {
123+
auto result = co_await db->exec(APreparedQueryLiteral(u"SELECT RAND()"_s));
124+
if (result) {
125+
qDebug() << i << "coro result has value" << result->toJsonObject();
126+
} else {
127+
qDebug() << i << "coro result error" << result.error();
128+
}
129+
}
130+
131+
qApp->quit();
132+
};
133+
callTerminatorEarly();
134+
}
135+
136+
if (false) {
137+
auto multipleQueries = []() -> ACoroTerminator {
138+
auto _ = qScopeGuard([] { qDebug() << "coro exited"; });
139+
qDebug() << "coro started prepared queries";
140+
141+
bool last = true;
142+
auto awaitable = APool::exec(u"SELECT date(); SELECT 123; SELECT 456"_s);
143+
do {
144+
auto result = co_await awaitable;
145+
if (result) {
146+
qDebug() << "coro result has value" << result->columnNames()
147+
<< result->toJsonObject();
148+
last = result->lastResultSet();
149+
} else {
150+
qDebug() << "coro result error" << result.error();
151+
break;
152+
}
153+
} while (!last);
154+
155+
qApp->quit();
156+
};
157+
158+
// auto db = APool::database();
159+
// db.exec(u"SELECT date(); SELECT 123; SELECT 456"_s, nullptr, [](AResult &result) {
160+
// qDebug() << "1result" << result.query();
161+
// qDebug() << "2result" << result.lastResultSet();
162+
// qDebug() << "3result" << result.toJsonObject();
163+
// if (result.lastResultSet()) {
164+
// qApp->quit();
165+
// }
166+
// });
167+
multipleQueries();
168+
}
169+
170+
app.exec();
171+
}

0 commit comments

Comments
 (0)