1 | // Copyright (C) 2020 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qsql_mysql_p.h" |
5 | |
6 | #include <qcoreapplication.h> |
7 | #include <qvariant.h> |
8 | #include <qvarlengtharray.h> |
9 | #include <qdatetime.h> |
10 | #include <qdebug.h> |
11 | #include <qfile.h> |
12 | #include <qlist.h> |
13 | #include <qsqlerror.h> |
14 | #include <qsqlfield.h> |
15 | #include <qsqlindex.h> |
16 | #include <qsqlquery.h> |
17 | #include <qsqlrecord.h> |
18 | #include <qstringlist.h> |
19 | #include <QtSql/private/qsqldriver_p.h> |
20 | #include <QtSql/private/qsqlresult_p.h> |
21 | |
22 | #ifdef Q_OS_WIN32 |
23 | // comment the next line out if you want to use MySQL/embedded on Win32 systems. |
24 | // note that it will crash if you don't statically link to the mysql/e library! |
25 | # define Q_NO_MYSQL_EMBEDDED |
26 | #endif |
27 | |
28 | Q_DECLARE_METATYPE(MYSQL_RES*) |
29 | Q_DECLARE_METATYPE(MYSQL*) |
30 | Q_DECLARE_METATYPE(MYSQL_STMT*) |
31 | |
32 | // MYSQL_TYPE_JSON was introduced with MySQL 5.7.9 |
33 | #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID < 50709 |
34 | #define MYSQL_TYPE_JSON 245 |
35 | #endif |
36 | |
37 | // MySQL above version 8 removed my_bool typedef while MariaDB kept it, |
38 | // by redefining it we can regain source compatibility. |
39 | using my_bool = decltype(mysql_stmt_bind_result(stmt: nullptr, bnd: nullptr)); |
40 | |
41 | // this is a copy of the old MYSQL_TIME before an additional integer was added in |
42 | // 8.0.27.0. This kills the sanity check during retrieving this struct from mysql |
43 | // when another libmysql version is used during runtime than during compile time |
44 | struct QT_MYSQL_TIME |
45 | { |
46 | unsigned int year, month, day, hour, minute, second; |
47 | unsigned long second_part; /**< microseconds */ |
48 | my_bool neg; |
49 | enum enum_mysql_timestamp_type time_type; |
50 | }; |
51 | |
52 | QT_BEGIN_NAMESPACE |
53 | |
54 | using namespace Qt::StringLiterals; |
55 | |
56 | class QMYSQLDriverPrivate : public QSqlDriverPrivate |
57 | { |
58 | Q_DECLARE_PUBLIC(QMYSQLDriver) |
59 | |
60 | public: |
61 | QMYSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MySqlServer) |
62 | {} |
63 | MYSQL *mysql = nullptr; |
64 | QString dbName; |
65 | bool preparedQuerysEnabled = false; |
66 | }; |
67 | |
68 | static inline QVariant qDateFromString(const QString &val) |
69 | { |
70 | #if !QT_CONFIG(datestring) |
71 | Q_UNUSED(val); |
72 | return QVariant(val); |
73 | #else |
74 | if (val.isEmpty()) |
75 | return QVariant(QDate()); |
76 | return QVariant(QDate::fromString(string: val, format: Qt::ISODate)); |
77 | #endif |
78 | } |
79 | |
80 | static inline QVariant qTimeFromString(const QString &val) |
81 | { |
82 | #if !QT_CONFIG(datestring) |
83 | Q_UNUSED(val); |
84 | return QVariant(val); |
85 | #else |
86 | if (val.isEmpty()) |
87 | return QVariant(QTime()); |
88 | return QVariant(QTime::fromString(string: val, format: Qt::ISODate)); |
89 | #endif |
90 | } |
91 | |
92 | static inline QVariant qDateTimeFromString(QString &val) |
93 | { |
94 | #if !QT_CONFIG(datestring) |
95 | Q_UNUSED(val); |
96 | return QVariant(val); |
97 | #else |
98 | if (val.isEmpty()) |
99 | return QVariant(QDateTime()); |
100 | if (val.size() == 14) |
101 | // TIMESTAMPS have the format yyyyMMddhhmmss |
102 | val.insert(i: 4, c: u'-').insert(i: 7, c: u'-').insert(i: 10, c: u'T').insert(i: 13, c: u':').insert(i: 16, c: u':'); |
103 | return QVariant(QDateTime::fromString(string: val, format: Qt::ISODate)); |
104 | #endif |
105 | } |
106 | |
107 | // check if this client and server version of MySQL/MariaDB support prepared statements |
108 | static inline bool checkPreparedQueries(MYSQL *mysql) |
109 | { |
110 | std::unique_ptr<MYSQL_STMT, decltype(&mysql_stmt_close)> stmt(mysql_stmt_init(mysql), &mysql_stmt_close); |
111 | if (!stmt) |
112 | return false; |
113 | |
114 | static const char dummyQuery[] = "SELECT ? + ?" ; |
115 | if (mysql_stmt_prepare(stmt: stmt.get(), query: dummyQuery, length: sizeof(dummyQuery) - 1)) |
116 | return false; |
117 | |
118 | return mysql_stmt_param_count(stmt: stmt.get()) == 2; |
119 | } |
120 | |
121 | class QMYSQLResultPrivate; |
122 | |
123 | class QMYSQLResult : public QSqlResult |
124 | { |
125 | Q_DECLARE_PRIVATE(QMYSQLResult) |
126 | friend class QMYSQLDriver; |
127 | |
128 | public: |
129 | explicit QMYSQLResult(const QMYSQLDriver *db); |
130 | ~QMYSQLResult(); |
131 | |
132 | QVariant handle() const override; |
133 | protected: |
134 | void cleanup(); |
135 | bool fetch(int i) override; |
136 | bool fetchNext() override; |
137 | bool fetchLast() override; |
138 | bool fetchFirst() override; |
139 | QVariant data(int field) override; |
140 | bool isNull(int field) override; |
141 | bool reset (const QString& query) override; |
142 | int size() override; |
143 | int numRowsAffected() override; |
144 | QVariant lastInsertId() const override; |
145 | QSqlRecord record() const override; |
146 | void virtual_hook(int id, void *data) override; |
147 | bool nextResult() override; |
148 | void detachFromResultSet() override; |
149 | |
150 | bool prepare(const QString &stmt) override; |
151 | bool exec() override; |
152 | }; |
153 | |
154 | class QMYSQLResultPrivate: public QSqlResultPrivate |
155 | { |
156 | Q_DECLARE_PUBLIC(QMYSQLResult) |
157 | |
158 | public: |
159 | Q_DECLARE_SQLDRIVER_PRIVATE(QMYSQLDriver) |
160 | |
161 | using QSqlResultPrivate::QSqlResultPrivate; |
162 | |
163 | bool bindInValues(); |
164 | void bindBlobs(); |
165 | |
166 | MYSQL_RES *result = nullptr; |
167 | MYSQL_ROW row; |
168 | |
169 | struct QMyField |
170 | { |
171 | char *outField = nullptr; |
172 | const MYSQL_FIELD *myField = nullptr; |
173 | QMetaType type = QMetaType(); |
174 | my_bool nullIndicator = false; |
175 | ulong bufLength = 0ul; |
176 | }; |
177 | |
178 | QList<QMyField> fields; |
179 | |
180 | MYSQL_STMT *stmt = nullptr; |
181 | MYSQL_RES *meta = nullptr; |
182 | |
183 | MYSQL_BIND *inBinds = nullptr; |
184 | MYSQL_BIND *outBinds = nullptr; |
185 | |
186 | int rowsAffected = 0; |
187 | bool hasBlobs = false; |
188 | bool preparedQuery = false; |
189 | }; |
190 | |
191 | static QSqlError qMakeError(const QString &err, QSqlError::ErrorType type, |
192 | const QMYSQLDriverPrivate *p) |
193 | { |
194 | const char *cerr = p->mysql ? mysql_error(mysql: p->mysql) : nullptr; |
195 | return QSqlError("QMYSQL: "_L1 + err, |
196 | QString::fromUtf8(utf8: cerr), |
197 | type, QString::number(mysql_errno(mysql: p->mysql))); |
198 | } |
199 | |
200 | |
201 | static QMetaType qDecodeMYSQLType(enum_field_types mysqltype, uint flags) |
202 | { |
203 | QMetaType::Type type; |
204 | switch (mysqltype) { |
205 | case MYSQL_TYPE_TINY: |
206 | type = (flags & UNSIGNED_FLAG) ? QMetaType::UChar : QMetaType::Char; |
207 | break; |
208 | case MYSQL_TYPE_SHORT: |
209 | type = (flags & UNSIGNED_FLAG) ? QMetaType::UShort : QMetaType::Short; |
210 | break; |
211 | case MYSQL_TYPE_LONG: |
212 | case MYSQL_TYPE_INT24: |
213 | type = (flags & UNSIGNED_FLAG) ? QMetaType::UInt : QMetaType::Int; |
214 | break; |
215 | case MYSQL_TYPE_YEAR: |
216 | type = QMetaType::Int; |
217 | break; |
218 | case MYSQL_TYPE_BIT: |
219 | case MYSQL_TYPE_LONGLONG: |
220 | type = (flags & UNSIGNED_FLAG) ? QMetaType::ULongLong : QMetaType::LongLong; |
221 | break; |
222 | case MYSQL_TYPE_FLOAT: |
223 | case MYSQL_TYPE_DOUBLE: |
224 | case MYSQL_TYPE_DECIMAL: |
225 | case MYSQL_TYPE_NEWDECIMAL: |
226 | type = QMetaType::Double; |
227 | break; |
228 | case MYSQL_TYPE_DATE: |
229 | type = QMetaType::QDate; |
230 | break; |
231 | case MYSQL_TYPE_TIME: |
232 | // A time field can be within the range '-838:59:59' to '838:59:59' so |
233 | // use QString instead of QTime since QTime is limited to 24 hour clock |
234 | type = QMetaType::QString; |
235 | break; |
236 | case MYSQL_TYPE_DATETIME: |
237 | case MYSQL_TYPE_TIMESTAMP: |
238 | type = QMetaType::QDateTime; |
239 | break; |
240 | case MYSQL_TYPE_STRING: |
241 | case MYSQL_TYPE_VAR_STRING: |
242 | case MYSQL_TYPE_BLOB: |
243 | case MYSQL_TYPE_TINY_BLOB: |
244 | case MYSQL_TYPE_MEDIUM_BLOB: |
245 | case MYSQL_TYPE_LONG_BLOB: |
246 | case MYSQL_TYPE_GEOMETRY: |
247 | case MYSQL_TYPE_JSON: |
248 | type = (flags & BINARY_FLAG) ? QMetaType::QByteArray : QMetaType::QString; |
249 | break; |
250 | case MYSQL_TYPE_ENUM: |
251 | case MYSQL_TYPE_SET: |
252 | type = QMetaType::QString; |
253 | break; |
254 | default: // needed because there are more enum values which are not available in all headers |
255 | type = QMetaType::QString; |
256 | break; |
257 | } |
258 | return QMetaType(type); |
259 | } |
260 | |
261 | static QSqlField qToField(MYSQL_FIELD *field) |
262 | { |
263 | QSqlField f(QString::fromUtf8(utf8: field->name), |
264 | qDecodeMYSQLType(mysqltype: field->type, flags: field->flags), |
265 | QString::fromUtf8(utf8: field->table)); |
266 | f.setRequired(IS_NOT_NULL(field->flags)); |
267 | f.setLength(field->length); |
268 | f.setPrecision(field->decimals); |
269 | f.setSqlType(field->type); |
270 | f.setAutoValue(field->flags & AUTO_INCREMENT_FLAG); |
271 | return f; |
272 | } |
273 | |
274 | static QSqlError qMakeStmtError(const QString &err, QSqlError::ErrorType type, |
275 | MYSQL_STMT *stmt) |
276 | { |
277 | const char *cerr = mysql_stmt_error(stmt); |
278 | return QSqlError("QMYSQL: "_L1 + err, |
279 | QString::fromLatin1(ba: cerr), |
280 | type, QString::number(mysql_stmt_errno(stmt))); |
281 | } |
282 | |
283 | static bool qIsBlob(enum_field_types t) |
284 | { |
285 | return t == MYSQL_TYPE_TINY_BLOB |
286 | || t == MYSQL_TYPE_BLOB |
287 | || t == MYSQL_TYPE_MEDIUM_BLOB |
288 | || t == MYSQL_TYPE_LONG_BLOB |
289 | || t == MYSQL_TYPE_JSON; |
290 | } |
291 | |
292 | static bool qIsTimeOrDate(enum_field_types t) |
293 | { |
294 | // *not* MYSQL_TYPE_TIME because its range is bigger than QTime |
295 | // (see above) |
296 | return t == MYSQL_TYPE_DATE || t == MYSQL_TYPE_DATETIME || t == MYSQL_TYPE_TIMESTAMP; |
297 | } |
298 | |
299 | static bool qIsInteger(int t) |
300 | { |
301 | return t == QMetaType::Char || t == QMetaType::UChar |
302 | || t == QMetaType::Short || t == QMetaType::UShort |
303 | || t == QMetaType::Int || t == QMetaType::UInt |
304 | || t == QMetaType::LongLong || t == QMetaType::ULongLong; |
305 | } |
306 | |
307 | static inline bool qIsBitfield(enum_field_types type) |
308 | { |
309 | return type == MYSQL_TYPE_BIT; |
310 | } |
311 | |
312 | void QMYSQLResultPrivate::bindBlobs() |
313 | { |
314 | for (int i = 0; i < fields.size(); ++i) { |
315 | const MYSQL_FIELD *fieldInfo = fields.at(i).myField; |
316 | if (qIsBlob(t: inBinds[i].buffer_type) && meta && fieldInfo) { |
317 | MYSQL_BIND *bind = &inBinds[i]; |
318 | bind->buffer_length = fieldInfo->max_length; |
319 | delete[] static_cast<char*>(bind->buffer); |
320 | bind->buffer = new char[fieldInfo->max_length]; |
321 | fields[i].outField = static_cast<char*>(bind->buffer); |
322 | } |
323 | } |
324 | } |
325 | |
326 | bool QMYSQLResultPrivate::bindInValues() |
327 | { |
328 | if (!meta) |
329 | meta = mysql_stmt_result_metadata(stmt); |
330 | if (!meta) |
331 | return false; |
332 | |
333 | fields.resize(size: mysql_num_fields(res: meta)); |
334 | |
335 | inBinds = new MYSQL_BIND[fields.size()]; |
336 | memset(s: inBinds, c: 0, n: fields.size() * sizeof(MYSQL_BIND)); |
337 | |
338 | const MYSQL_FIELD *fieldInfo; |
339 | |
340 | int i = 0; |
341 | while((fieldInfo = mysql_fetch_field(result: meta))) { |
342 | MYSQL_BIND *bind = &inBinds[i]; |
343 | |
344 | QMyField &f = fields[i]; |
345 | f.myField = fieldInfo; |
346 | bind->buffer_length = f.bufLength = fieldInfo->length + 1; |
347 | bind->buffer_type = fieldInfo->type; |
348 | f.type = qDecodeMYSQLType(mysqltype: fieldInfo->type, flags: fieldInfo->flags); |
349 | if (qIsBlob(t: fieldInfo->type)) { |
350 | // the size of a blob-field is available as soon as we call |
351 | // mysql_stmt_store_result() |
352 | // after mysql_stmt_exec() in QMYSQLResult::exec() |
353 | bind->buffer_length = f.bufLength = 0; |
354 | hasBlobs = true; |
355 | } else if (qIsTimeOrDate(t: fieldInfo->type)) { |
356 | bind->buffer_length = f.bufLength = sizeof(QT_MYSQL_TIME); |
357 | } else if (qIsInteger(t: f.type.id())) { |
358 | bind->buffer_length = f.bufLength = 8; |
359 | } else { |
360 | bind->buffer_type = MYSQL_TYPE_STRING; |
361 | } |
362 | |
363 | bind->is_null = &f.nullIndicator; |
364 | bind->length = &f.bufLength; |
365 | bind->is_unsigned = fieldInfo->flags & UNSIGNED_FLAG ? 1 : 0; |
366 | |
367 | char *field = bind->buffer_length ? new char[bind->buffer_length + 1]{} : nullptr; |
368 | bind->buffer = f.outField = field; |
369 | |
370 | ++i; |
371 | } |
372 | return true; |
373 | } |
374 | |
375 | QMYSQLResult::QMYSQLResult(const QMYSQLDriver* db) |
376 | : QSqlResult(*new QMYSQLResultPrivate(this, db)) |
377 | { |
378 | } |
379 | |
380 | QMYSQLResult::~QMYSQLResult() |
381 | { |
382 | cleanup(); |
383 | } |
384 | |
385 | QVariant QMYSQLResult::handle() const |
386 | { |
387 | Q_D(const QMYSQLResult); |
388 | if (d->preparedQuery) |
389 | return d->meta ? QVariant::fromValue(value: d->meta) : QVariant::fromValue(value: d->stmt); |
390 | else |
391 | return QVariant::fromValue(value: d->result); |
392 | } |
393 | |
394 | void QMYSQLResult::cleanup() |
395 | { |
396 | Q_D(QMYSQLResult); |
397 | if (d->result) |
398 | mysql_free_result(result: d->result); |
399 | |
400 | // must iterate through leftover result sets from multi-selects or stored procedures |
401 | // if this isn't done subsequent queries will fail with "Commands out of sync" |
402 | while (driver() && d->drv_d_func()->mysql && mysql_next_result(mysql: d->drv_d_func()->mysql) == 0) { |
403 | MYSQL_RES *res = mysql_store_result(mysql: d->drv_d_func()->mysql); |
404 | if (res) |
405 | mysql_free_result(result: res); |
406 | } |
407 | |
408 | if (d->stmt) { |
409 | if (mysql_stmt_close(stmt: d->stmt)) |
410 | qWarning(msg: "QMYSQLResult::cleanup: unable to free statement handle" ); |
411 | d->stmt = 0; |
412 | } |
413 | |
414 | if (d->meta) { |
415 | mysql_free_result(result: d->meta); |
416 | d->meta = 0; |
417 | } |
418 | |
419 | for (const QMYSQLResultPrivate::QMyField &f : std::as_const(t&: d->fields)) |
420 | delete[] f.outField; |
421 | |
422 | if (d->outBinds) { |
423 | delete[] d->outBinds; |
424 | d->outBinds = 0; |
425 | } |
426 | |
427 | if (d->inBinds) { |
428 | delete[] d->inBinds; |
429 | d->inBinds = 0; |
430 | } |
431 | |
432 | d->hasBlobs = false; |
433 | d->fields.clear(); |
434 | d->result = nullptr; |
435 | d->row = nullptr; |
436 | setAt(-1); |
437 | setActive(false); |
438 | } |
439 | |
440 | bool QMYSQLResult::fetch(int i) |
441 | { |
442 | Q_D(QMYSQLResult); |
443 | if (!driver()) |
444 | return false; |
445 | if (isForwardOnly()) { // fake a forward seek |
446 | if (at() < i) { |
447 | int x = i - at(); |
448 | while (--x && fetchNext()) {}; |
449 | return fetchNext(); |
450 | } else { |
451 | return false; |
452 | } |
453 | } |
454 | if (at() == i) |
455 | return true; |
456 | if (d->preparedQuery) { |
457 | mysql_stmt_data_seek(stmt: d->stmt, offset: i); |
458 | |
459 | int nRC = mysql_stmt_fetch(stmt: d->stmt); |
460 | if (nRC) { |
461 | if (nRC == 1 || nRC == MYSQL_DATA_TRUNCATED) |
462 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
463 | key: "Unable to fetch data" ), type: QSqlError::StatementError, stmt: d->stmt)); |
464 | return false; |
465 | } |
466 | } else { |
467 | mysql_data_seek(result: d->result, offset: i); |
468 | d->row = mysql_fetch_row(result: d->result); |
469 | if (!d->row) |
470 | return false; |
471 | } |
472 | |
473 | setAt(i); |
474 | return true; |
475 | } |
476 | |
477 | bool QMYSQLResult::fetchNext() |
478 | { |
479 | Q_D(QMYSQLResult); |
480 | if (!driver()) |
481 | return false; |
482 | if (d->preparedQuery) { |
483 | int nRC = mysql_stmt_fetch(stmt: d->stmt); |
484 | if (nRC) { |
485 | if (nRC == 1 || nRC == MYSQL_DATA_TRUNCATED) |
486 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
487 | key: "Unable to fetch data" ), type: QSqlError::StatementError, stmt: d->stmt)); |
488 | return false; |
489 | } |
490 | } else { |
491 | d->row = mysql_fetch_row(result: d->result); |
492 | if (!d->row) |
493 | return false; |
494 | } |
495 | setAt(at() + 1); |
496 | return true; |
497 | } |
498 | |
499 | bool QMYSQLResult::fetchLast() |
500 | { |
501 | Q_D(QMYSQLResult); |
502 | if (!driver()) |
503 | return false; |
504 | if (isForwardOnly()) { // fake this since MySQL can't seek on forward only queries |
505 | bool success = fetchNext(); // did we move at all? |
506 | while (fetchNext()) {}; |
507 | return success; |
508 | } |
509 | |
510 | my_ulonglong numRows = d->preparedQuery ? mysql_stmt_num_rows(stmt: d->stmt) : mysql_num_rows(res: d->result); |
511 | if (at() == int(numRows)) |
512 | return true; |
513 | if (!numRows) |
514 | return false; |
515 | return fetch(i: numRows - 1); |
516 | } |
517 | |
518 | bool QMYSQLResult::fetchFirst() |
519 | { |
520 | if (at() == 0) |
521 | return true; |
522 | |
523 | if (isForwardOnly()) |
524 | return (at() == QSql::BeforeFirstRow) ? fetchNext() : false; |
525 | return fetch(i: 0); |
526 | } |
527 | |
528 | static inline uint64_t |
529 | qDecodeBitfield(const QMYSQLResultPrivate::QMyField &f, const char *outField) |
530 | { |
531 | // byte-aligned length |
532 | const auto numBytes = (f.myField->length + 7) / 8; |
533 | uint64_t val = 0; |
534 | for (unsigned long i = 0; i < numBytes && outField; ++i) { |
535 | uint64_t tmp = static_cast<uint8_t>(outField[i]); |
536 | val <<= 8; |
537 | val |= tmp; |
538 | } |
539 | return val; |
540 | } |
541 | |
542 | QVariant QMYSQLResult::data(int field) |
543 | { |
544 | Q_D(QMYSQLResult); |
545 | if (!isSelect() || field >= d->fields.size()) { |
546 | qWarning(msg: "QMYSQLResult::data: column %d out of range" , field); |
547 | return QVariant(); |
548 | } |
549 | |
550 | if (!driver()) |
551 | return QVariant(); |
552 | |
553 | my_ulonglong fieldLength = 0; |
554 | const QMYSQLResultPrivate::QMyField &f = d->fields.at(i: field); |
555 | QString val; |
556 | if (d->preparedQuery) { |
557 | if (f.nullIndicator) |
558 | return QVariant(f.type); |
559 | if (qIsBitfield(type: f.myField->type)) { |
560 | return QVariant::fromValue(value: qDecodeBitfield(f, outField: f.outField)); |
561 | } else if (qIsInteger(t: f.type.id())) { |
562 | QVariant variant(f.type, f.outField); |
563 | // we never want to return char variants here, see QTBUG-53397 |
564 | if (f.type.id() == QMetaType::UChar) |
565 | return variant.toUInt(); |
566 | else if (f.type.id() == QMetaType::Char) |
567 | return variant.toInt(); |
568 | return variant; |
569 | } else if (qIsTimeOrDate(t: f.myField->type) && f.bufLength >= sizeof(QT_MYSQL_TIME)) { |
570 | auto t = reinterpret_cast<const QT_MYSQL_TIME *>(f.outField); |
571 | QDate date; |
572 | QTime time; |
573 | if (f.type.id() != QMetaType::QTime) |
574 | date = QDate(t->year, t->month, t->day); |
575 | if (f.type.id() != QMetaType::QDate) |
576 | time = QTime(t->hour, t->minute, t->second, t->second_part / 1000); |
577 | if (f.type.id() == QMetaType::QDateTime) |
578 | return QDateTime(date, time); |
579 | else if (f.type.id() == QMetaType::QDate) |
580 | return date; |
581 | else |
582 | return time; |
583 | } |
584 | |
585 | if (f.type.id() != QMetaType::QByteArray) |
586 | val = QString::fromUtf8(utf8: f.outField, size: f.bufLength); |
587 | } else { |
588 | if (d->row[field] == nullptr) { |
589 | // NULL value |
590 | return QVariant(f.type); |
591 | } |
592 | |
593 | if (qIsBitfield(type: f.myField->type)) |
594 | return QVariant::fromValue(value: qDecodeBitfield(f, outField: d->row[field])); |
595 | |
596 | fieldLength = mysql_fetch_lengths(result: d->result)[field]; |
597 | |
598 | if (f.type.id() != QMetaType::QByteArray) |
599 | val = QString::fromUtf8(utf8: d->row[field], size: fieldLength); |
600 | } |
601 | |
602 | switch (f.type.id()) { |
603 | case QMetaType::LongLong: |
604 | return QVariant(val.toLongLong()); |
605 | case QMetaType::ULongLong: |
606 | return QVariant(val.toULongLong()); |
607 | case QMetaType::Char: |
608 | case QMetaType::Short: |
609 | case QMetaType::Int: |
610 | return QVariant(val.toInt()); |
611 | case QMetaType::UChar: |
612 | case QMetaType::UShort: |
613 | case QMetaType::UInt: |
614 | return QVariant(val.toUInt()); |
615 | case QMetaType::Double: { |
616 | QVariant v; |
617 | bool ok=false; |
618 | double dbl = val.toDouble(ok: &ok); |
619 | switch(numericalPrecisionPolicy()) { |
620 | case QSql::LowPrecisionInt32: |
621 | v=QVariant(dbl).toInt(); |
622 | break; |
623 | case QSql::LowPrecisionInt64: |
624 | v = QVariant(dbl).toLongLong(); |
625 | break; |
626 | case QSql::LowPrecisionDouble: |
627 | v = QVariant(dbl); |
628 | break; |
629 | case QSql::HighPrecision: |
630 | default: |
631 | v = val; |
632 | ok = true; |
633 | break; |
634 | } |
635 | if (ok) |
636 | return v; |
637 | return QVariant(); |
638 | } |
639 | case QMetaType::QDate: |
640 | return qDateFromString(val); |
641 | case QMetaType::QTime: |
642 | return qTimeFromString(val); |
643 | case QMetaType::QDateTime: |
644 | return qDateTimeFromString(val); |
645 | case QMetaType::QByteArray: { |
646 | |
647 | QByteArray ba; |
648 | if (d->preparedQuery) { |
649 | ba = QByteArray(f.outField, f.bufLength); |
650 | } else { |
651 | ba = QByteArray(d->row[field], fieldLength); |
652 | } |
653 | return QVariant(ba); |
654 | } |
655 | case QMetaType::QString: |
656 | default: |
657 | return QVariant(val); |
658 | } |
659 | Q_UNREACHABLE(); |
660 | } |
661 | |
662 | bool QMYSQLResult::isNull(int field) |
663 | { |
664 | Q_D(const QMYSQLResult); |
665 | if (field < 0 || field >= d->fields.size()) |
666 | return true; |
667 | if (d->preparedQuery) |
668 | return d->fields.at(i: field).nullIndicator; |
669 | else |
670 | return d->row[field] == nullptr; |
671 | } |
672 | |
673 | bool QMYSQLResult::reset (const QString& query) |
674 | { |
675 | Q_D(QMYSQLResult); |
676 | if (!driver() || !driver()->isOpen() || driver()->isOpenError()) |
677 | return false; |
678 | |
679 | d->preparedQuery = false; |
680 | |
681 | cleanup(); |
682 | |
683 | const QByteArray encQuery = query.toUtf8(); |
684 | if (mysql_real_query(mysql: d->drv_d_func()->mysql, q: encQuery.data(), length: encQuery.size())) { |
685 | setLastError(qMakeError(err: QCoreApplication::translate(context: "QMYSQLResult" , key: "Unable to execute query" ), |
686 | type: QSqlError::StatementError, p: d->drv_d_func())); |
687 | return false; |
688 | } |
689 | d->result = mysql_store_result(mysql: d->drv_d_func()->mysql); |
690 | if (!d->result && mysql_field_count(mysql: d->drv_d_func()->mysql) > 0) { |
691 | setLastError(qMakeError(err: QCoreApplication::translate(context: "QMYSQLResult" , key: "Unable to store result" ), |
692 | type: QSqlError::StatementError, p: d->drv_d_func())); |
693 | return false; |
694 | } |
695 | int numFields = mysql_field_count(mysql: d->drv_d_func()->mysql); |
696 | setSelect(numFields != 0); |
697 | d->fields.resize(size: numFields); |
698 | d->rowsAffected = mysql_affected_rows(mysql: d->drv_d_func()->mysql); |
699 | |
700 | if (isSelect()) { |
701 | for(int i = 0; i < numFields; i++) { |
702 | MYSQL_FIELD* field = mysql_fetch_field_direct(res: d->result, fieldnr: i); |
703 | d->fields[i].type = qDecodeMYSQLType(mysqltype: field->type, flags: field->flags); |
704 | d->fields[i].myField = field; |
705 | } |
706 | setAt(QSql::BeforeFirstRow); |
707 | } |
708 | setActive(true); |
709 | return isActive(); |
710 | } |
711 | |
712 | int QMYSQLResult::size() |
713 | { |
714 | Q_D(const QMYSQLResult); |
715 | if (driver() && isSelect()) |
716 | if (d->preparedQuery) |
717 | return mysql_stmt_num_rows(stmt: d->stmt); |
718 | else |
719 | return int(mysql_num_rows(res: d->result)); |
720 | else |
721 | return -1; |
722 | } |
723 | |
724 | int QMYSQLResult::numRowsAffected() |
725 | { |
726 | Q_D(const QMYSQLResult); |
727 | return d->rowsAffected; |
728 | } |
729 | |
730 | void QMYSQLResult::detachFromResultSet() |
731 | { |
732 | Q_D(QMYSQLResult); |
733 | |
734 | if (d->preparedQuery) { |
735 | mysql_stmt_free_result(stmt: d->stmt); |
736 | } |
737 | } |
738 | |
739 | QVariant QMYSQLResult::lastInsertId() const |
740 | { |
741 | Q_D(const QMYSQLResult); |
742 | if (!isActive() || !driver()) |
743 | return QVariant(); |
744 | |
745 | if (d->preparedQuery) { |
746 | quint64 id = mysql_stmt_insert_id(stmt: d->stmt); |
747 | if (id) |
748 | return QVariant(id); |
749 | } else { |
750 | quint64 id = mysql_insert_id(mysql: d->drv_d_func()->mysql); |
751 | if (id) |
752 | return QVariant(id); |
753 | } |
754 | return QVariant(); |
755 | } |
756 | |
757 | QSqlRecord QMYSQLResult::record() const |
758 | { |
759 | Q_D(const QMYSQLResult); |
760 | QSqlRecord info; |
761 | MYSQL_RES *res; |
762 | if (!isActive() || !isSelect() || !driver()) |
763 | return info; |
764 | |
765 | res = d->preparedQuery ? d->meta : d->result; |
766 | |
767 | if (!mysql_errno(mysql: d->drv_d_func()->mysql)) { |
768 | mysql_field_seek(result: res, offset: 0); |
769 | MYSQL_FIELD* field = mysql_fetch_field(result: res); |
770 | while (field) { |
771 | info.append(field: qToField(field)); |
772 | field = mysql_fetch_field(result: res); |
773 | } |
774 | } |
775 | mysql_field_seek(result: res, offset: 0); |
776 | return info; |
777 | } |
778 | |
779 | bool QMYSQLResult::nextResult() |
780 | { |
781 | Q_D(QMYSQLResult); |
782 | if (!driver()) |
783 | return false; |
784 | |
785 | setAt(-1); |
786 | setActive(false); |
787 | |
788 | if (d->result && isSelect()) |
789 | mysql_free_result(result: d->result); |
790 | d->result = 0; |
791 | setSelect(false); |
792 | |
793 | for (const QMYSQLResultPrivate::QMyField &f : std::as_const(t&: d->fields)) |
794 | delete[] f.outField; |
795 | d->fields.clear(); |
796 | |
797 | int status = mysql_next_result(mysql: d->drv_d_func()->mysql); |
798 | if (status > 0) { |
799 | setLastError(qMakeError(err: QCoreApplication::translate(context: "QMYSQLResult" , key: "Unable to execute next query" ), |
800 | type: QSqlError::StatementError, p: d->drv_d_func())); |
801 | return false; |
802 | } else if (status == -1) { |
803 | return false; // No more result sets |
804 | } |
805 | |
806 | d->result = mysql_store_result(mysql: d->drv_d_func()->mysql); |
807 | unsigned int numFields = mysql_field_count(mysql: d->drv_d_func()->mysql); |
808 | if (!d->result && numFields > 0) { |
809 | setLastError(qMakeError(err: QCoreApplication::translate(context: "QMYSQLResult" , key: "Unable to store next result" ), |
810 | type: QSqlError::StatementError, p: d->drv_d_func())); |
811 | return false; |
812 | } |
813 | |
814 | setSelect(numFields > 0); |
815 | d->fields.resize(size: numFields); |
816 | d->rowsAffected = mysql_affected_rows(mysql: d->drv_d_func()->mysql); |
817 | |
818 | if (isSelect()) { |
819 | for (unsigned int i = 0; i < numFields; i++) { |
820 | MYSQL_FIELD *field = mysql_fetch_field_direct(res: d->result, fieldnr: i); |
821 | d->fields[i].type = qDecodeMYSQLType(mysqltype: field->type, flags: field->flags); |
822 | d->fields[i].myField = field; |
823 | } |
824 | } |
825 | |
826 | setActive(true); |
827 | return true; |
828 | } |
829 | |
830 | void QMYSQLResult::virtual_hook(int id, void *data) |
831 | { |
832 | QSqlResult::virtual_hook(id, data); |
833 | } |
834 | |
835 | static QT_MYSQL_TIME *toMySqlDate(QDate date, QTime time, int type) |
836 | { |
837 | Q_ASSERT(type == QMetaType::QTime || type == QMetaType::QDate |
838 | || type == QMetaType::QDateTime); |
839 | |
840 | auto myTime = new QT_MYSQL_TIME{}; |
841 | |
842 | if (type == QMetaType::QTime || type == QMetaType::QDateTime) { |
843 | myTime->hour = time.hour(); |
844 | myTime->minute = time.minute(); |
845 | myTime->second = time.second(); |
846 | myTime->second_part = time.msec() * 1000; |
847 | } |
848 | if (type == QMetaType::QDate || type == QMetaType::QDateTime) { |
849 | myTime->year = date.year(); |
850 | myTime->month = date.month(); |
851 | myTime->day = date.day(); |
852 | } |
853 | |
854 | return myTime; |
855 | } |
856 | |
857 | bool QMYSQLResult::prepare(const QString& query) |
858 | { |
859 | Q_D(QMYSQLResult); |
860 | if (!driver()) |
861 | return false; |
862 | |
863 | cleanup(); |
864 | if (!d->drv_d_func()->preparedQuerysEnabled) |
865 | return QSqlResult::prepare(query); |
866 | |
867 | int r; |
868 | |
869 | if (query.isEmpty()) |
870 | return false; |
871 | |
872 | if (!d->stmt) |
873 | d->stmt = mysql_stmt_init(mysql: d->drv_d_func()->mysql); |
874 | if (!d->stmt) { |
875 | setLastError(qMakeError(err: QCoreApplication::translate(context: "QMYSQLResult" , key: "Unable to prepare statement" ), |
876 | type: QSqlError::StatementError, p: d->drv_d_func())); |
877 | return false; |
878 | } |
879 | |
880 | const QByteArray encQuery = query.toUtf8(); |
881 | r = mysql_stmt_prepare(stmt: d->stmt, query: encQuery.constData(), length: encQuery.size()); |
882 | if (r != 0) { |
883 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
884 | key: "Unable to prepare statement" ), type: QSqlError::StatementError, stmt: d->stmt)); |
885 | cleanup(); |
886 | return false; |
887 | } |
888 | |
889 | const auto paramCount = mysql_stmt_param_count(stmt: d->stmt); |
890 | if (paramCount > 0) // allocate memory for outvalues |
891 | d->outBinds = new MYSQL_BIND[paramCount](); |
892 | |
893 | setSelect(d->bindInValues()); |
894 | d->preparedQuery = true; |
895 | return true; |
896 | } |
897 | |
898 | bool QMYSQLResult::exec() |
899 | { |
900 | Q_D(QMYSQLResult); |
901 | if (!driver()) |
902 | return false; |
903 | if (!d->preparedQuery) |
904 | return QSqlResult::exec(); |
905 | if (!d->stmt) |
906 | return false; |
907 | |
908 | int r = 0; |
909 | QList<QT_MYSQL_TIME *> timeVector; |
910 | QList<QByteArray> stringVector; |
911 | QList<my_bool> nullVector; |
912 | |
913 | const QList<QVariant> values = boundValues(); |
914 | |
915 | r = mysql_stmt_reset(stmt: d->stmt); |
916 | if (r != 0) { |
917 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
918 | key: "Unable to reset statement" ), type: QSqlError::StatementError, stmt: d->stmt)); |
919 | return false; |
920 | } |
921 | |
922 | if (mysql_stmt_param_count(stmt: d->stmt) > 0 && |
923 | mysql_stmt_param_count(stmt: d->stmt) == (uint)values.size()) { |
924 | |
925 | nullVector.resize(size: values.size()); |
926 | for (qsizetype i = 0; i < values.size(); ++i) { |
927 | const QVariant &val = boundValues().at(i); |
928 | void *data = const_cast<void *>(val.constData()); |
929 | |
930 | MYSQL_BIND* currBind = &d->outBinds[i]; |
931 | |
932 | nullVector[i] = static_cast<my_bool>(QSqlResultPrivate::isVariantNull(variant: val)); |
933 | currBind->is_null = &nullVector[i]; |
934 | currBind->length = 0; |
935 | currBind->is_unsigned = 0; |
936 | |
937 | switch (val.userType()) { |
938 | case QMetaType::QByteArray: |
939 | currBind->buffer_type = MYSQL_TYPE_BLOB; |
940 | currBind->buffer = const_cast<char *>(val.toByteArray().constData()); |
941 | currBind->buffer_length = val.toByteArray().size(); |
942 | break; |
943 | |
944 | case QMetaType::QTime: |
945 | case QMetaType::QDate: |
946 | case QMetaType::QDateTime: { |
947 | QT_MYSQL_TIME *myTime = toMySqlDate(date: val.toDate(), time: val.toTime(), type: val.userType()); |
948 | timeVector.append(t: myTime); |
949 | |
950 | currBind->buffer = myTime; |
951 | switch (val.userType()) { |
952 | case QMetaType::QTime: |
953 | currBind->buffer_type = MYSQL_TYPE_TIME; |
954 | myTime->time_type = MYSQL_TIMESTAMP_TIME; |
955 | break; |
956 | case QMetaType::QDate: |
957 | currBind->buffer_type = MYSQL_TYPE_DATE; |
958 | myTime->time_type = MYSQL_TIMESTAMP_DATE; |
959 | break; |
960 | case QMetaType::QDateTime: |
961 | currBind->buffer_type = MYSQL_TYPE_DATETIME; |
962 | myTime->time_type = MYSQL_TIMESTAMP_DATETIME; |
963 | break; |
964 | default: |
965 | break; |
966 | } |
967 | currBind->buffer_length = sizeof(QT_MYSQL_TIME); |
968 | currBind->length = 0; |
969 | break; } |
970 | case QMetaType::UInt: |
971 | case QMetaType::Int: |
972 | currBind->buffer_type = MYSQL_TYPE_LONG; |
973 | currBind->buffer = data; |
974 | currBind->buffer_length = sizeof(int); |
975 | currBind->is_unsigned = (val.userType() != QMetaType::Int); |
976 | break; |
977 | case QMetaType::Bool: |
978 | currBind->buffer_type = MYSQL_TYPE_TINY; |
979 | currBind->buffer = data; |
980 | currBind->buffer_length = sizeof(bool); |
981 | currBind->is_unsigned = false; |
982 | break; |
983 | case QMetaType::Double: |
984 | currBind->buffer_type = MYSQL_TYPE_DOUBLE; |
985 | currBind->buffer = data; |
986 | currBind->buffer_length = sizeof(double); |
987 | break; |
988 | case QMetaType::LongLong: |
989 | case QMetaType::ULongLong: |
990 | currBind->buffer_type = MYSQL_TYPE_LONGLONG; |
991 | currBind->buffer = data; |
992 | currBind->buffer_length = sizeof(qint64); |
993 | currBind->is_unsigned = (val.userType() == QMetaType::ULongLong); |
994 | break; |
995 | case QMetaType::QString: |
996 | default: { |
997 | QByteArray ba = val.toString().toUtf8(); |
998 | stringVector.append(t: ba); |
999 | currBind->buffer_type = MYSQL_TYPE_STRING; |
1000 | currBind->buffer = const_cast<char *>(ba.constData()); |
1001 | currBind->buffer_length = ba.size(); |
1002 | break; } |
1003 | } |
1004 | } |
1005 | |
1006 | r = mysql_stmt_bind_param(stmt: d->stmt, bnd: d->outBinds); |
1007 | if (r != 0) { |
1008 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
1009 | key: "Unable to bind value" ), type: QSqlError::StatementError, stmt: d->stmt)); |
1010 | qDeleteAll(c: timeVector); |
1011 | return false; |
1012 | } |
1013 | } |
1014 | r = mysql_stmt_execute(stmt: d->stmt); |
1015 | |
1016 | qDeleteAll(c: timeVector); |
1017 | |
1018 | if (r != 0) { |
1019 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
1020 | key: "Unable to execute statement" ), type: QSqlError::StatementError, stmt: d->stmt)); |
1021 | return false; |
1022 | } |
1023 | //if there is meta-data there is also data |
1024 | setSelect(d->meta); |
1025 | |
1026 | d->rowsAffected = mysql_stmt_affected_rows(stmt: d->stmt); |
1027 | |
1028 | if (isSelect()) { |
1029 | my_bool update_max_length = true; |
1030 | |
1031 | r = mysql_stmt_bind_result(stmt: d->stmt, bnd: d->inBinds); |
1032 | if (r != 0) { |
1033 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
1034 | key: "Unable to bind outvalues" ), type: QSqlError::StatementError, stmt: d->stmt)); |
1035 | return false; |
1036 | } |
1037 | if (d->hasBlobs) |
1038 | mysql_stmt_attr_set(stmt: d->stmt, attr_type: STMT_ATTR_UPDATE_MAX_LENGTH, attr: &update_max_length); |
1039 | |
1040 | r = mysql_stmt_store_result(stmt: d->stmt); |
1041 | if (r != 0) { |
1042 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
1043 | key: "Unable to store statement results" ), type: QSqlError::StatementError, stmt: d->stmt)); |
1044 | return false; |
1045 | } |
1046 | |
1047 | if (d->hasBlobs) { |
1048 | // mysql_stmt_store_result() with STMT_ATTR_UPDATE_MAX_LENGTH set to true crashes |
1049 | // when called without a preceding call to mysql_stmt_bind_result() |
1050 | // in versions < 4.1.8 |
1051 | d->bindBlobs(); |
1052 | r = mysql_stmt_bind_result(stmt: d->stmt, bnd: d->inBinds); |
1053 | if (r != 0) { |
1054 | setLastError(qMakeStmtError(err: QCoreApplication::translate(context: "QMYSQLResult" , |
1055 | key: "Unable to bind outvalues" ), type: QSqlError::StatementError, stmt: d->stmt)); |
1056 | return false; |
1057 | } |
1058 | } |
1059 | setAt(QSql::BeforeFirstRow); |
1060 | } |
1061 | setActive(true); |
1062 | return true; |
1063 | } |
1064 | |
1065 | ///////////////////////////////////////////////////////// |
1066 | |
1067 | static int qMySqlConnectionCount = 0; |
1068 | static bool qMySqlInitHandledByUser = false; |
1069 | |
1070 | static void qLibraryInit() |
1071 | { |
1072 | #ifndef Q_NO_MYSQL_EMBEDDED |
1073 | if (qMySqlInitHandledByUser || qMySqlConnectionCount > 1) |
1074 | return; |
1075 | |
1076 | if (mysql_library_init(argc: 0, argv: 0, groups: 0)) { |
1077 | qWarning(msg: "QMYSQLDriver::qServerInit: unable to start server." ); |
1078 | } |
1079 | #endif // Q_NO_MYSQL_EMBEDDED |
1080 | |
1081 | #if defined(MARIADB_BASE_VERSION) || defined(MARIADB_VERSION_ID) |
1082 | qAddPostRoutine([]() { mysql_server_end(); }); |
1083 | #endif |
1084 | } |
1085 | |
1086 | static void qLibraryEnd() |
1087 | { |
1088 | #if !defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID) |
1089 | # if !defined(Q_NO_MYSQL_EMBEDDED) |
1090 | mysql_library_end(); |
1091 | # endif |
1092 | #endif |
1093 | } |
1094 | |
1095 | QMYSQLDriver::QMYSQLDriver(QObject * parent) |
1096 | : QSqlDriver(*new QMYSQLDriverPrivate, parent) |
1097 | { |
1098 | init(); |
1099 | qLibraryInit(); |
1100 | } |
1101 | |
1102 | /*! |
1103 | Create a driver instance with the open connection handle, \a con. |
1104 | The instance's parent (owner) is \a parent. |
1105 | */ |
1106 | |
1107 | QMYSQLDriver::QMYSQLDriver(MYSQL * con, QObject * parent) |
1108 | : QSqlDriver(*new QMYSQLDriverPrivate, parent) |
1109 | { |
1110 | Q_D(QMYSQLDriver); |
1111 | init(); |
1112 | if (con) { |
1113 | d->mysql = con; |
1114 | setOpen(true); |
1115 | setOpenError(false); |
1116 | if (qMySqlConnectionCount == 1) |
1117 | qMySqlInitHandledByUser = true; |
1118 | } else { |
1119 | qLibraryInit(); |
1120 | } |
1121 | } |
1122 | |
1123 | void QMYSQLDriver::init() |
1124 | { |
1125 | Q_D(QMYSQLDriver); |
1126 | d->mysql = 0; |
1127 | qMySqlConnectionCount++; |
1128 | } |
1129 | |
1130 | QMYSQLDriver::~QMYSQLDriver() |
1131 | { |
1132 | qMySqlConnectionCount--; |
1133 | if (qMySqlConnectionCount == 0 && !qMySqlInitHandledByUser) |
1134 | qLibraryEnd(); |
1135 | } |
1136 | |
1137 | bool QMYSQLDriver::hasFeature(DriverFeature f) const |
1138 | { |
1139 | Q_D(const QMYSQLDriver); |
1140 | switch (f) { |
1141 | case Transactions: |
1142 | if (d->mysql) { |
1143 | if ((d->mysql->server_capabilities & CLIENT_TRANSACTIONS) == CLIENT_TRANSACTIONS) |
1144 | return true; |
1145 | } |
1146 | return false; |
1147 | case NamedPlaceholders: |
1148 | case BatchOperations: |
1149 | case SimpleLocking: |
1150 | case EventNotifications: |
1151 | case FinishQuery: |
1152 | case CancelQuery: |
1153 | return false; |
1154 | case QuerySize: |
1155 | case BLOB: |
1156 | case LastInsertId: |
1157 | case Unicode: |
1158 | case LowPrecisionNumbers: |
1159 | return true; |
1160 | case PreparedQueries: |
1161 | case PositionalPlaceholders: |
1162 | return d->preparedQuerysEnabled; |
1163 | case MultipleResultSets: |
1164 | return true; |
1165 | } |
1166 | return false; |
1167 | } |
1168 | |
1169 | static void setOptionFlag(uint &optionFlags, QStringView opt) |
1170 | { |
1171 | if (opt == "CLIENT_COMPRESS"_L1 ) |
1172 | optionFlags |= CLIENT_COMPRESS; |
1173 | else if (opt == "CLIENT_FOUND_ROWS"_L1 ) |
1174 | optionFlags |= CLIENT_FOUND_ROWS; |
1175 | else if (opt == "CLIENT_IGNORE_SPACE"_L1 ) |
1176 | optionFlags |= CLIENT_IGNORE_SPACE; |
1177 | else if (opt == "CLIENT_INTERACTIVE"_L1 ) |
1178 | optionFlags |= CLIENT_INTERACTIVE; |
1179 | else if (opt == "CLIENT_NO_SCHEMA"_L1 ) |
1180 | optionFlags |= CLIENT_NO_SCHEMA; |
1181 | else if (opt == "CLIENT_ODBC"_L1 ) |
1182 | optionFlags |= CLIENT_ODBC; |
1183 | else if (opt == "CLIENT_SSL"_L1 ) |
1184 | qWarning(msg: "QMYSQLDriver: MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT and MYSQL_OPT_SSL_CA should be used instead of CLIENT_SSL." ); |
1185 | else |
1186 | qWarning(msg: "QMYSQLDriver::open: Unknown connect option '%s'" , opt.toLocal8Bit().constData()); |
1187 | } |
1188 | |
1189 | static bool setOptionString(MYSQL *mysql, mysql_option option, QStringView v) |
1190 | { |
1191 | return mysql_options(mysql, option, arg: v.toUtf8().constData()) == 0; |
1192 | } |
1193 | |
1194 | static bool setOptionInt(MYSQL *mysql, mysql_option option, QStringView v) |
1195 | { |
1196 | bool bOk; |
1197 | const auto val = v.toInt(ok: &bOk); |
1198 | return bOk ? mysql_options(mysql, option, arg: &val) == 0 : false; |
1199 | } |
1200 | |
1201 | static bool setOptionBool(MYSQL *mysql, mysql_option option, QStringView v) |
1202 | { |
1203 | bool val = (v.isEmpty() || v == "TRUE"_L1 || v == "1"_L1 ); |
1204 | return mysql_options(mysql, option, arg: &val) == 0; |
1205 | } |
1206 | |
1207 | // MYSQL_OPT_SSL_MODE was introduced with MySQL 5.7.11 |
1208 | #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID) |
1209 | static bool setOptionSslMode(MYSQL *mysql, mysql_option option, QStringView v) |
1210 | { |
1211 | mysql_ssl_mode sslMode = SSL_MODE_DISABLED; |
1212 | if (v == "DISABLED"_L1 || v == "SSL_MODE_DISABLED"_L1 ) |
1213 | sslMode = SSL_MODE_DISABLED; |
1214 | else if (v == "PREFERRED"_L1 || v == "SSL_MODE_PREFERRED"_L1 ) |
1215 | sslMode = SSL_MODE_PREFERRED; |
1216 | else if (v == "REQUIRED"_L1 || v == "SSL_MODE_REQUIRED"_L1 ) |
1217 | sslMode = SSL_MODE_REQUIRED; |
1218 | else if (v == "VERIFY_CA"_L1 || v == "SSL_MODE_VERIFY_CA"_L1 ) |
1219 | sslMode = SSL_MODE_VERIFY_CA; |
1220 | else if (v == "VERIFY_IDENTITY"_L1 || v == "SSL_MODE_VERIFY_IDENTITY"_L1 ) |
1221 | sslMode = SSL_MODE_VERIFY_IDENTITY; |
1222 | else |
1223 | qWarning() << "Unknown ssl mode '" << v << "' - using SSL_MODE_DISABLED" ; |
1224 | return mysql_options(mysql, option, arg: &sslMode) == 0; |
1225 | } |
1226 | #endif |
1227 | |
1228 | static bool setOptionProtocol(MYSQL *mysql, mysql_option option, QStringView v) |
1229 | { |
1230 | mysql_protocol_type proto = MYSQL_PROTOCOL_DEFAULT; |
1231 | if (v == "TCP"_L1 || v == "MYSQL_PROTOCOL_TCP"_L1 ) |
1232 | proto = MYSQL_PROTOCOL_TCP; |
1233 | else if (v == "SOCKET"_L1 || v == "MYSQL_PROTOCOL_SOCKET"_L1 ) |
1234 | proto = MYSQL_PROTOCOL_SOCKET; |
1235 | else if (v == "PIPE"_L1 || v == "MYSQL_PROTOCOL_PIPE"_L1 ) |
1236 | proto = MYSQL_PROTOCOL_PIPE; |
1237 | else if (v == "MEMORY"_L1 || v == "MYSQL_PROTOCOL_MEMORY"_L1 ) |
1238 | proto = MYSQL_PROTOCOL_MEMORY; |
1239 | else if (v == "DEFAULT"_L1 || v == "MYSQL_PROTOCOL_DEFAULT"_L1 ) |
1240 | proto = MYSQL_PROTOCOL_DEFAULT; |
1241 | else |
1242 | qWarning() << "Unknown protocol '" << v << "' - using MYSQL_PROTOCOL_DEFAULT" ; |
1243 | return mysql_options(mysql, option, arg: &proto) == 0; |
1244 | } |
1245 | |
1246 | bool QMYSQLDriver::open(const QString &db, |
1247 | const QString &user, |
1248 | const QString &password, |
1249 | const QString &host, |
1250 | int port, |
1251 | const QString &connOpts) |
1252 | { |
1253 | Q_D(QMYSQLDriver); |
1254 | if (isOpen()) |
1255 | close(); |
1256 | |
1257 | if (!(d->mysql = mysql_init(mysql: nullptr))) { |
1258 | setLastError(qMakeError(err: tr(s: "Unable to allocate a MYSQL object" ), |
1259 | type: QSqlError::ConnectionError, p: d)); |
1260 | setOpenError(true); |
1261 | return false; |
1262 | } |
1263 | |
1264 | typedef bool (*SetOptionFunc)(MYSQL*, mysql_option, QStringView); |
1265 | struct mysqloptions { |
1266 | QLatin1StringView key; |
1267 | mysql_option option; |
1268 | SetOptionFunc func; |
1269 | }; |
1270 | const mysqloptions options[] = { |
1271 | {.key: "SSL_KEY"_L1 , .option: MYSQL_OPT_SSL_KEY, .func: setOptionString}, |
1272 | {.key: "SSL_CERT"_L1 , .option: MYSQL_OPT_SSL_CERT, .func: setOptionString}, |
1273 | {.key: "SSL_CA"_L1 , .option: MYSQL_OPT_SSL_CA, .func: setOptionString}, |
1274 | {.key: "SSL_CAPATH"_L1 , .option: MYSQL_OPT_SSL_CAPATH, .func: setOptionString}, |
1275 | {.key: "SSL_CIPHER"_L1 , .option: MYSQL_OPT_SSL_CIPHER, .func: setOptionString}, |
1276 | {.key: "MYSQL_OPT_SSL_KEY"_L1 , .option: MYSQL_OPT_SSL_KEY, .func: setOptionString}, |
1277 | {.key: "MYSQL_OPT_SSL_CERT"_L1 , .option: MYSQL_OPT_SSL_CERT, .func: setOptionString}, |
1278 | {.key: "MYSQL_OPT_SSL_CA"_L1 , .option: MYSQL_OPT_SSL_CA, .func: setOptionString}, |
1279 | {.key: "MYSQL_OPT_SSL_CAPATH"_L1 , .option: MYSQL_OPT_SSL_CAPATH, .func: setOptionString}, |
1280 | {.key: "MYSQL_OPT_SSL_CIPHER"_L1 , .option: MYSQL_OPT_SSL_CIPHER, .func: setOptionString}, |
1281 | {.key: "MYSQL_OPT_SSL_CRL"_L1 , .option: MYSQL_OPT_SSL_CRL, .func: setOptionString}, |
1282 | {.key: "MYSQL_OPT_SSL_CRLPATH"_L1 , .option: MYSQL_OPT_SSL_CRLPATH, .func: setOptionString}, |
1283 | #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50710 |
1284 | {.key: "MYSQL_OPT_TLS_VERSION"_L1 , .option: MYSQL_OPT_TLS_VERSION, .func: setOptionString}, |
1285 | #endif |
1286 | #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID) |
1287 | {.key: "MYSQL_OPT_SSL_MODE"_L1 , .option: MYSQL_OPT_SSL_MODE, .func: setOptionSslMode}, |
1288 | #endif |
1289 | {.key: "MYSQL_OPT_CONNECT_TIMEOUT"_L1 , .option: MYSQL_OPT_CONNECT_TIMEOUT, .func: setOptionInt}, |
1290 | {.key: "MYSQL_OPT_READ_TIMEOUT"_L1 , .option: MYSQL_OPT_READ_TIMEOUT, .func: setOptionInt}, |
1291 | {.key: "MYSQL_OPT_WRITE_TIMEOUT"_L1 , .option: MYSQL_OPT_WRITE_TIMEOUT, .func: setOptionInt}, |
1292 | {.key: "MYSQL_OPT_RECONNECT"_L1 , .option: MYSQL_OPT_RECONNECT, .func: setOptionBool}, |
1293 | {.key: "MYSQL_OPT_LOCAL_INFILE"_L1 , .option: MYSQL_OPT_LOCAL_INFILE, .func: setOptionInt}, |
1294 | {.key: "MYSQL_OPT_PROTOCOL"_L1 , .option: MYSQL_OPT_PROTOCOL, .func: setOptionProtocol}, |
1295 | {.key: "MYSQL_SHARED_MEMORY_BASE_NAME"_L1 , .option: MYSQL_SHARED_MEMORY_BASE_NAME, .func: setOptionString}, |
1296 | }; |
1297 | auto trySetOption = [&](const QStringView &key, const QStringView &value) -> bool { |
1298 | for (const mysqloptions &opt : options) { |
1299 | if (key == opt.key) { |
1300 | if (!opt.func(d->mysql, opt.option, value)) { |
1301 | qWarning(msg: "QMYSQLDriver::open: Could not set connect option value '%s' to '%s'" , |
1302 | key.toLocal8Bit().constData(), value.toLocal8Bit().constData()); |
1303 | } |
1304 | return true; |
1305 | } |
1306 | } |
1307 | return false; |
1308 | }; |
1309 | |
1310 | /* This is a hack to get MySQL's stored procedure support working. |
1311 | Since a stored procedure _may_ return multiple result sets, |
1312 | we have to enable CLIEN_MULTI_STATEMENTS here, otherwise _any_ |
1313 | stored procedure call will fail. |
1314 | */ |
1315 | unsigned int optionFlags = CLIENT_MULTI_STATEMENTS; |
1316 | const QList<QStringView> opts(QStringView(connOpts).split(sep: u';', behavior: Qt::SkipEmptyParts)); |
1317 | QString unixSocket; |
1318 | |
1319 | // extract the real options from the string |
1320 | for (const auto &option : opts) { |
1321 | const QStringView sv = QStringView(option).trimmed(); |
1322 | qsizetype idx; |
1323 | if ((idx = sv.indexOf(c: u'=')) != -1) { |
1324 | const QStringView key = sv.left(n: idx).trimmed(); |
1325 | const QStringView val = sv.mid(pos: idx + 1).trimmed(); |
1326 | if (trySetOption(key, val)) |
1327 | continue; |
1328 | else if (key == "UNIX_SOCKET"_L1 ) |
1329 | unixSocket = val.toString(); |
1330 | else if (val == "TRUE"_L1 || val == "1"_L1 ) |
1331 | setOptionFlag(optionFlags, opt: key); |
1332 | else |
1333 | qWarning(msg: "QMYSQLDriver::open: Illegal connect option value '%s'" , |
1334 | sv.toLocal8Bit().constData()); |
1335 | } else { |
1336 | setOptionFlag(optionFlags, opt: sv); |
1337 | } |
1338 | } |
1339 | |
1340 | // try utf8 with non BMP first, utf8 (BMP only) if that fails |
1341 | static const char wanted_charsets[][8] = { "utf8mb4" , "utf8" }; |
1342 | #ifdef MARIADB_VERSION_ID |
1343 | MARIADB_CHARSET_INFO *cs = nullptr; |
1344 | for (const char *p : wanted_charsets) { |
1345 | cs = mariadb_get_charset_by_name(p); |
1346 | if (cs) { |
1347 | d->mysql->charset = cs; |
1348 | break; |
1349 | } |
1350 | } |
1351 | #else |
1352 | // dummy |
1353 | struct { |
1354 | const char *csname; |
1355 | } *cs = nullptr; |
1356 | #endif |
1357 | |
1358 | MYSQL *mysql = mysql_real_connect(mysql: d->mysql, |
1359 | host: host.isNull() ? nullptr : host.toUtf8().constData(), |
1360 | user: user.isNull() ? nullptr : user.toUtf8().constData(), |
1361 | passwd: password.isNull() ? nullptr : password.toUtf8().constData(), |
1362 | db: db.isNull() ? nullptr : db.toUtf8().constData(), |
1363 | port: (port > -1) ? port : 0, |
1364 | unix_socket: unixSocket.isNull() ? nullptr : unixSocket.toUtf8().constData(), |
1365 | clientflag: optionFlags); |
1366 | |
1367 | if (mysql != d->mysql) { |
1368 | setLastError(qMakeError(err: tr(s: "Unable to connect" ), |
1369 | type: QSqlError::ConnectionError, p: d)); |
1370 | mysql_close(sock: d->mysql); |
1371 | d->mysql = nullptr; |
1372 | setOpenError(true); |
1373 | return false; |
1374 | } |
1375 | |
1376 | // now ask the server to match the charset we selected |
1377 | if (!cs || mysql_set_character_set(mysql: d->mysql, csname: cs->csname) != 0) { |
1378 | bool ok = false; |
1379 | for (const char *p : wanted_charsets) { |
1380 | if (mysql_set_character_set(mysql: d->mysql, csname: p) == 0) { |
1381 | ok = true; |
1382 | break; |
1383 | } |
1384 | } |
1385 | if (!ok) |
1386 | qWarning(msg: "MySQL: Unable to set the client character set to utf8 (\"%s\"). Using '%s' instead." , |
1387 | mysql_error(mysql: d->mysql), |
1388 | mysql_character_set_name(mysql: d->mysql)); |
1389 | } |
1390 | |
1391 | if (!db.isEmpty() && mysql_select_db(mysql: d->mysql, db: db.toUtf8().constData())) { |
1392 | setLastError(qMakeError(err: tr(s: "Unable to open database '%1'" ).arg(a: db), type: QSqlError::ConnectionError, p: d)); |
1393 | mysql_close(sock: d->mysql); |
1394 | setOpenError(true); |
1395 | return false; |
1396 | } |
1397 | |
1398 | d->preparedQuerysEnabled = checkPreparedQueries(mysql: d->mysql); |
1399 | d->dbName = db; |
1400 | |
1401 | #if QT_CONFIG(thread) |
1402 | mysql_thread_init(); |
1403 | #endif |
1404 | |
1405 | setOpen(true); |
1406 | setOpenError(false); |
1407 | return true; |
1408 | } |
1409 | |
1410 | void QMYSQLDriver::close() |
1411 | { |
1412 | Q_D(QMYSQLDriver); |
1413 | if (isOpen()) { |
1414 | #if QT_CONFIG(thread) |
1415 | mysql_thread_end(); |
1416 | #endif |
1417 | mysql_close(sock: d->mysql); |
1418 | d->mysql = nullptr; |
1419 | d->dbName.clear(); |
1420 | setOpen(false); |
1421 | setOpenError(false); |
1422 | } |
1423 | } |
1424 | |
1425 | QSqlResult *QMYSQLDriver::createResult() const |
1426 | { |
1427 | return new QMYSQLResult(this); |
1428 | } |
1429 | |
1430 | QStringList QMYSQLDriver::tables(QSql::TableType type) const |
1431 | { |
1432 | Q_D(const QMYSQLDriver); |
1433 | QStringList tl; |
1434 | QSqlQuery q(createResult()); |
1435 | if (type & QSql::Tables) { |
1436 | QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'BASE TABLE'"_L1 ; |
1437 | q.exec(query: sql); |
1438 | |
1439 | while (q.next()) |
1440 | tl.append(t: q.value(i: 0).toString()); |
1441 | } |
1442 | if (type & QSql::Views) { |
1443 | QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'VIEW'"_L1 ; |
1444 | q.exec(query: sql); |
1445 | |
1446 | while (q.next()) |
1447 | tl.append(t: q.value(i: 0).toString()); |
1448 | } |
1449 | return tl; |
1450 | } |
1451 | |
1452 | QSqlIndex QMYSQLDriver::primaryIndex(const QString &tablename) const |
1453 | { |
1454 | QSqlIndex idx; |
1455 | if (!isOpen()) |
1456 | return idx; |
1457 | |
1458 | QSqlQuery i(createResult()); |
1459 | QString stmt("show index from %1;"_L1 ); |
1460 | QSqlRecord fil = record(tablename); |
1461 | i.exec(query: stmt.arg(a: escapeIdentifier(identifier: tablename, type: QSqlDriver::TableName))); |
1462 | while (i.isActive() && i.next()) { |
1463 | if (i.value(i: 2).toString() == "PRIMARY"_L1 ) { |
1464 | idx.append(field: fil.field(name: i.value(i: 4).toString())); |
1465 | idx.setCursorName(i.value(i: 0).toString()); |
1466 | idx.setName(i.value(i: 2).toString()); |
1467 | } |
1468 | } |
1469 | |
1470 | return idx; |
1471 | } |
1472 | |
1473 | QSqlRecord QMYSQLDriver::record(const QString &tablename) const |
1474 | { |
1475 | Q_D(const QMYSQLDriver); |
1476 | const QString table = stripDelimiters(identifier: tablename, type: QSqlDriver::TableName); |
1477 | |
1478 | QSqlRecord info; |
1479 | if (!isOpen()) |
1480 | return info; |
1481 | MYSQL_RES *r = mysql_list_fields(mysql: d->mysql, table: table.toUtf8().constData(), wild: nullptr); |
1482 | if (!r) |
1483 | return info; |
1484 | |
1485 | MYSQL_FIELD *field; |
1486 | while ((field = mysql_fetch_field(result: r))) |
1487 | info.append(field: qToField(field)); |
1488 | mysql_free_result(result: r); |
1489 | return info; |
1490 | } |
1491 | |
1492 | QVariant QMYSQLDriver::handle() const |
1493 | { |
1494 | Q_D(const QMYSQLDriver); |
1495 | return QVariant::fromValue(value: d->mysql); |
1496 | } |
1497 | |
1498 | bool QMYSQLDriver::beginTransaction() |
1499 | { |
1500 | Q_D(QMYSQLDriver); |
1501 | if (!isOpen()) { |
1502 | qWarning(msg: "QMYSQLDriver::beginTransaction: Database not open" ); |
1503 | return false; |
1504 | } |
1505 | if (mysql_query(mysql: d->mysql, q: "BEGIN WORK" )) { |
1506 | setLastError(qMakeError(err: tr(s: "Unable to begin transaction" ), |
1507 | type: QSqlError::StatementError, p: d)); |
1508 | return false; |
1509 | } |
1510 | return true; |
1511 | } |
1512 | |
1513 | bool QMYSQLDriver::commitTransaction() |
1514 | { |
1515 | Q_D(QMYSQLDriver); |
1516 | if (!isOpen()) { |
1517 | qWarning(msg: "QMYSQLDriver::commitTransaction: Database not open" ); |
1518 | return false; |
1519 | } |
1520 | if (mysql_query(mysql: d->mysql, q: "COMMIT" )) { |
1521 | setLastError(qMakeError(err: tr(s: "Unable to commit transaction" ), |
1522 | type: QSqlError::StatementError, p: d)); |
1523 | return false; |
1524 | } |
1525 | return true; |
1526 | } |
1527 | |
1528 | bool QMYSQLDriver::rollbackTransaction() |
1529 | { |
1530 | Q_D(QMYSQLDriver); |
1531 | if (!isOpen()) { |
1532 | qWarning(msg: "QMYSQLDriver::rollbackTransaction: Database not open" ); |
1533 | return false; |
1534 | } |
1535 | if (mysql_query(mysql: d->mysql, q: "ROLLBACK" )) { |
1536 | setLastError(qMakeError(err: tr(s: "Unable to rollback transaction" ), |
1537 | type: QSqlError::StatementError, p: d)); |
1538 | return false; |
1539 | } |
1540 | return true; |
1541 | } |
1542 | |
1543 | QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const |
1544 | { |
1545 | Q_D(const QMYSQLDriver); |
1546 | QString r; |
1547 | if (field.isNull()) { |
1548 | r = QStringLiteral("NULL" ); |
1549 | } else { |
1550 | switch (field.metaType().id()) { |
1551 | case QMetaType::Double: |
1552 | r = QString::number(field.value().toDouble(), format: 'g', precision: field.precision()); |
1553 | break; |
1554 | case QMetaType::QString: |
1555 | // Escape '\' characters |
1556 | r = QSqlDriver::formatValue(field, trimStrings); |
1557 | r.replace(before: "\\"_L1 , after: "\\\\"_L1 ); |
1558 | break; |
1559 | case QMetaType::QByteArray: |
1560 | if (isOpen()) { |
1561 | const QByteArray ba = field.value().toByteArray(); |
1562 | // buffer has to be at least length*2+1 bytes |
1563 | QVarLengthArray<char, 512> buffer(ba.size() * 2 + 1); |
1564 | auto escapedSize = mysql_real_escape_string(mysql: d->mysql, to: buffer.data(), from: ba.data(), length: ba.size()); |
1565 | r.reserve(asize: escapedSize + 3); |
1566 | r = u'\'' + QString::fromUtf8(utf8: buffer.data(), size: escapedSize) + u'\''; |
1567 | break; |
1568 | } else { |
1569 | qWarning(msg: "QMYSQLDriver::formatValue: Database not open" ); |
1570 | } |
1571 | Q_FALLTHROUGH(); |
1572 | case QMetaType::QDateTime: |
1573 | if (QDateTime dt = field.value().toDateTime(); dt.isValid()) { |
1574 | // MySQL format doesn't like the "Z" at the end, but does allow |
1575 | // "+00:00" starting in version 8.0.19. However, if we got here, |
1576 | // it's because the MySQL server is too old for prepared queries |
1577 | // in the first place, so it won't understand timezones either. |
1578 | // Besides, MYSQL_TIME does not support timezones, so match it. |
1579 | r = u'\'' + |
1580 | dt.date().toString(format: Qt::ISODate) + |
1581 | u'T' + |
1582 | dt.time().toString(f: Qt::ISODate) + |
1583 | u'\''; |
1584 | } |
1585 | break; |
1586 | default: |
1587 | r = QSqlDriver::formatValue(field, trimStrings); |
1588 | } |
1589 | } |
1590 | return r; |
1591 | } |
1592 | |
1593 | QString QMYSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) const |
1594 | { |
1595 | QString res = identifier; |
1596 | if (!identifier.isEmpty() && !identifier.startsWith(c: u'`') && !identifier.endsWith(c: u'`')) { |
1597 | res.replace(c: u'.', after: "`.`"_L1 ); |
1598 | res = u'`' + res + u'`'; |
1599 | } |
1600 | return res; |
1601 | } |
1602 | |
1603 | bool QMYSQLDriver::isIdentifierEscaped(const QString &identifier, IdentifierType type) const |
1604 | { |
1605 | Q_UNUSED(type); |
1606 | return identifier.size() > 2 |
1607 | && identifier.startsWith(c: u'`') //left delimited |
1608 | && identifier.endsWith(c: u'`'); //right delimited |
1609 | } |
1610 | |
1611 | QT_END_NAMESPACE |
1612 | |
1613 | #include "moc_qsql_mysql_p.cpp" |
1614 | |