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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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