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