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
28Q_DECLARE_METATYPE(MYSQL_RES*)
29Q_DECLARE_METATYPE(MYSQL*)
30Q_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.
39using 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
44struct 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
52QT_BEGIN_NAMESPACE
53
54using namespace Qt::StringLiterals;
55
56class QMYSQLDriverPrivate : public QSqlDriverPrivate
57{
58 Q_DECLARE_PUBLIC(QMYSQLDriver)
59
60public:
61 QMYSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MySqlServer)
62 {}
63 MYSQL *mysql = nullptr;
64 QString dbName;
65 bool preparedQuerysEnabled = false;
66};
67
68static 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
80static 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
92static 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
108static 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
121class QMYSQLResultPrivate;
122
123class QMYSQLResult : public QSqlResult
124{
125 Q_DECLARE_PRIVATE(QMYSQLResult)
126 friend class QMYSQLDriver;
127
128public:
129 explicit QMYSQLResult(const QMYSQLDriver *db);
130 ~QMYSQLResult();
131
132 QVariant handle() const override;
133protected:
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
154class QMYSQLResultPrivate: public QSqlResultPrivate
155{
156 Q_DECLARE_PUBLIC(QMYSQLResult)
157
158public:
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
191static 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
201static 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
261static 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
274static 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
283static 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
292static 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
299static 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
307static inline bool qIsBitfield(enum_field_types type)
308{
309 return type == MYSQL_TYPE_BIT;
310}
311
312void 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
326bool 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
375QMYSQLResult::QMYSQLResult(const QMYSQLDriver* db)
376 : QSqlResult(*new QMYSQLResultPrivate(this, db))
377{
378}
379
380QMYSQLResult::~QMYSQLResult()
381{
382 cleanup();
383}
384
385QVariant 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
394void 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
440bool 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
477bool 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
499bool 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
518bool 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
528static inline uint64_t
529qDecodeBitfield(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
542QVariant 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
662bool 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
673bool 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
712int 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
724int QMYSQLResult::numRowsAffected()
725{
726 Q_D(const QMYSQLResult);
727 return d->rowsAffected;
728}
729
730void QMYSQLResult::detachFromResultSet()
731{
732 Q_D(QMYSQLResult);
733
734 if (d->preparedQuery) {
735 mysql_stmt_free_result(stmt: d->stmt);
736 }
737}
738
739QVariant 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
757QSqlRecord 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
779bool 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
830void QMYSQLResult::virtual_hook(int id, void *data)
831{
832 QSqlResult::virtual_hook(id, data);
833}
834
835static 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
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(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
898bool 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
1067static int qMySqlConnectionCount = 0;
1068static bool qMySqlInitHandledByUser = false;
1069
1070static 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
1086static 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
1095QMYSQLDriver::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
1107QMYSQLDriver::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
1123void QMYSQLDriver::init()
1124{
1125 Q_D(QMYSQLDriver);
1126 d->mysql = 0;
1127 qMySqlConnectionCount++;
1128}
1129
1130QMYSQLDriver::~QMYSQLDriver()
1131{
1132 qMySqlConnectionCount--;
1133 if (qMySqlConnectionCount == 0 && !qMySqlInitHandledByUser)
1134 qLibraryEnd();
1135}
1136
1137bool 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
1169static 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
1189static bool setOptionString(MYSQL *mysql, mysql_option option, QStringView v)
1190{
1191 return mysql_options(mysql, option, arg: v.toUtf8().constData()) == 0;
1192}
1193
1194static 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
1201static 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)
1209static 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
1228static 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
1246bool 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
1410void 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
1425QSqlResult *QMYSQLDriver::createResult() const
1426{
1427 return new QMYSQLResult(this);
1428}
1429
1430QStringList 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
1452QSqlIndex 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
1473QSqlRecord 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
1492QVariant QMYSQLDriver::handle() const
1493{
1494 Q_D(const QMYSQLDriver);
1495 return QVariant::fromValue(value: d->mysql);
1496}
1497
1498bool 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
1513bool 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
1528bool 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
1543QString 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
1593QString 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
1603bool 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
1611QT_END_NAMESPACE
1612
1613#include "moc_qsql_mysql_p.cpp"
1614

source code of qtbase/src/plugins/sqldrivers/mysql/qsql_mysql.cpp