summaryrefslogtreecommitdiffstats
path: root/src/plugins/sqldrivers/mysql/qsql_mysql.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/sqldrivers/mysql/qsql_mysql.cpp')
-rw-r--r--src/plugins/sqldrivers/mysql/qsql_mysql.cpp556
1 files changed, 346 insertions, 210 deletions
diff --git a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp
index 3e5eeff360..cfd4931b46 100644
--- a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp
+++ b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp
@@ -10,12 +10,14 @@
#include <qdebug.h>
#include <qfile.h>
#include <qlist.h>
+#include <qloggingcategory.h>
#include <qsqlerror.h>
#include <qsqlfield.h>
#include <qsqlindex.h>
#include <qsqlquery.h>
#include <qsqlrecord.h>
#include <qstringlist.h>
+#include <qtimezone.h>
#include <QtSql/private/qsqldriver_p.h>
#include <QtSql/private/qsqlresult_p.h>
@@ -29,12 +31,30 @@ Q_DECLARE_METATYPE(MYSQL_RES*)
Q_DECLARE_METATYPE(MYSQL*)
Q_DECLARE_METATYPE(MYSQL_STMT*)
+// MYSQL_TYPE_JSON was introduced with MySQL 5.7.9
+#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID < 50709
+#define MYSQL_TYPE_JSON 245
+#endif
+
// MySQL above version 8 removed my_bool typedef while MariaDB kept it,
// by redefining it we can regain source compatibility.
using my_bool = decltype(mysql_stmt_bind_result(nullptr, nullptr));
+// this is a copy of the old MYSQL_TIME before an additional integer was added in
+// 8.0.27.0. This kills the sanity check during retrieving this struct from mysql
+// when another libmysql version is used during runtime than during compile time
+struct QT_MYSQL_TIME
+{
+ unsigned int year, month, day, hour, minute, second;
+ unsigned long second_part; /**< microseconds */
+ my_bool neg;
+ enum enum_mysql_timestamp_type time_type;
+};
+
QT_BEGIN_NAMESPACE
+static Q_LOGGING_CATEGORY(lcMysql, "qt.sql.mysql")
+
using namespace Qt::StringLiterals;
class QMYSQLDriverPrivate : public QSqlDriverPrivate
@@ -45,6 +65,7 @@ public:
QMYSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MySqlServer)
{}
MYSQL *mysql = nullptr;
+ QString dbName;
bool preparedQuerysEnabled = false;
};
@@ -80,9 +101,15 @@ static inline QVariant qDateTimeFromString(QString &val)
#else
if (val.isEmpty())
return QVariant(QDateTime());
+
+ // TIMESTAMPS have either the format "yyyyMMddhhmmss" or "yyyy-MM-dd
+ // hh:mm:ss". QDateTime::fromString() can convert the latter, but not the
+ // former, so adapt it if necessary.
if (val.size() == 14)
- // TIMESTAMPS have the format yyyyMMddhhmmss
val.insert(4, u'-').insert(7, u'-').insert(10, u'T').insert(13, u':').insert(16, u':');
+
+ if (!val.endsWith(u'Z'))
+ val.append(u'Z'); // make UTC
return QVariant(QDateTime::fromString(val, Qt::ISODate));
#endif
}
@@ -101,6 +128,18 @@ static inline bool checkPreparedQueries(MYSQL *mysql)
return mysql_stmt_param_count(stmt.get()) == 2;
}
+// used with prepared queries and bound arguments
+static inline void setUtcTimeZone(MYSQL *mysql)
+{
+ std::unique_ptr<MYSQL_STMT, decltype(&mysql_stmt_close)> stmt(mysql_stmt_init(mysql), &mysql_stmt_close);
+ if (!stmt)
+ return;
+
+ static const char query[] = "SET time_zone = '+00:00'";
+ if (mysql_stmt_prepare(stmt.get(), query, sizeof(query) - 1))
+ mysql_stmt_execute(stmt.get());
+}
+
class QMYSQLResultPrivate;
class QMYSQLResult : public QSqlResult
@@ -171,68 +210,70 @@ public:
bool preparedQuery = false;
};
-static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type,
- const QMYSQLDriverPrivate* p)
+static QSqlError qMakeError(const QString &err, QSqlError::ErrorType type,
+ const QMYSQLDriverPrivate *p)
{
- const char *cerr = p->mysql ? mysql_error(p->mysql) : 0;
+ const char *cerr = p->mysql ? mysql_error(p->mysql) : nullptr;
return QSqlError("QMYSQL: "_L1 + err,
QString::fromUtf8(cerr),
type, QString::number(mysql_errno(p->mysql)));
}
-static QMetaType qDecodeMYSQLType(int mysqltype, uint flags)
+static QMetaType qDecodeMYSQLType(enum_field_types mysqltype, uint flags)
{
QMetaType::Type type;
switch (mysqltype) {
- case FIELD_TYPE_TINY :
+ case MYSQL_TYPE_TINY:
type = (flags & UNSIGNED_FLAG) ? QMetaType::UChar : QMetaType::Char;
break;
- case FIELD_TYPE_SHORT :
+ case MYSQL_TYPE_SHORT:
type = (flags & UNSIGNED_FLAG) ? QMetaType::UShort : QMetaType::Short;
break;
- case FIELD_TYPE_LONG :
- case FIELD_TYPE_INT24 :
+ case MYSQL_TYPE_LONG:
+ case MYSQL_TYPE_INT24:
type = (flags & UNSIGNED_FLAG) ? QMetaType::UInt : QMetaType::Int;
break;
- case FIELD_TYPE_YEAR :
+ case MYSQL_TYPE_YEAR:
type = QMetaType::Int;
break;
- case FIELD_TYPE_LONGLONG :
+ case MYSQL_TYPE_BIT:
+ case MYSQL_TYPE_LONGLONG:
type = (flags & UNSIGNED_FLAG) ? QMetaType::ULongLong : QMetaType::LongLong;
break;
- case FIELD_TYPE_FLOAT :
- case FIELD_TYPE_DOUBLE :
- case FIELD_TYPE_DECIMAL :
-#if defined(FIELD_TYPE_NEWDECIMAL)
- case FIELD_TYPE_NEWDECIMAL:
-#endif
+ case MYSQL_TYPE_FLOAT:
+ case MYSQL_TYPE_DOUBLE:
+ case MYSQL_TYPE_DECIMAL:
+ case MYSQL_TYPE_NEWDECIMAL:
type = QMetaType::Double;
break;
- case FIELD_TYPE_DATE :
+ case MYSQL_TYPE_DATE:
type = QMetaType::QDate;
break;
- case FIELD_TYPE_TIME :
+ case MYSQL_TYPE_TIME:
// A time field can be within the range '-838:59:59' to '838:59:59' so
// use QString instead of QTime since QTime is limited to 24 hour clock
type = QMetaType::QString;
break;
- case FIELD_TYPE_DATETIME :
- case FIELD_TYPE_TIMESTAMP :
+ case MYSQL_TYPE_DATETIME:
+ case MYSQL_TYPE_TIMESTAMP:
type = QMetaType::QDateTime;
break;
- case FIELD_TYPE_STRING :
- case FIELD_TYPE_VAR_STRING :
- case FIELD_TYPE_BLOB :
- case FIELD_TYPE_TINY_BLOB :
- case FIELD_TYPE_MEDIUM_BLOB :
- case FIELD_TYPE_LONG_BLOB :
- case FIELD_TYPE_GEOMETRY :
+ case MYSQL_TYPE_STRING:
+ case MYSQL_TYPE_VAR_STRING:
+ case MYSQL_TYPE_BLOB:
+ case MYSQL_TYPE_TINY_BLOB:
+ case MYSQL_TYPE_MEDIUM_BLOB:
+ case MYSQL_TYPE_LONG_BLOB:
+ case MYSQL_TYPE_GEOMETRY:
+ case MYSQL_TYPE_JSON:
type = (flags & BINARY_FLAG) ? QMetaType::QByteArray : QMetaType::QString;
break;
- default:
- case FIELD_TYPE_ENUM :
- case FIELD_TYPE_SET :
+ case MYSQL_TYPE_ENUM:
+ case MYSQL_TYPE_SET:
+ type = QMetaType::QString;
+ break;
+ default: // needed because there are more enum values which are not available in all headers
type = QMetaType::QString;
break;
}
@@ -242,18 +283,17 @@ static QMetaType qDecodeMYSQLType(int mysqltype, uint flags)
static QSqlField qToField(MYSQL_FIELD *field)
{
QSqlField f(QString::fromUtf8(field->name),
- qDecodeMYSQLType(int(field->type), field->flags),
+ qDecodeMYSQLType(field->type, field->flags),
QString::fromUtf8(field->table));
f.setRequired(IS_NOT_NULL(field->flags));
f.setLength(field->length);
f.setPrecision(field->decimals);
- f.setSqlType(field->type);
f.setAutoValue(field->flags & AUTO_INCREMENT_FLAG);
return f;
}
-static QSqlError qMakeStmtError(const QString& err, QSqlError::ErrorType type,
- MYSQL_STMT* stmt)
+static QSqlError qMakeStmtError(const QString &err, QSqlError::ErrorType type,
+ MYSQL_STMT *stmt)
{
const char *cerr = mysql_stmt_error(stmt);
return QSqlError("QMYSQL: "_L1 + err,
@@ -261,15 +301,16 @@ static QSqlError qMakeStmtError(const QString& err, QSqlError::ErrorType type,
type, QString::number(mysql_stmt_errno(stmt)));
}
-static bool qIsBlob(int t)
+static bool qIsBlob(enum_field_types t)
{
return t == MYSQL_TYPE_TINY_BLOB
|| t == MYSQL_TYPE_BLOB
|| t == MYSQL_TYPE_MEDIUM_BLOB
- || t == MYSQL_TYPE_LONG_BLOB;
+ || t == MYSQL_TYPE_LONG_BLOB
+ || t == MYSQL_TYPE_JSON;
}
-static bool qIsTimeOrDate(int t)
+static bool qIsTimeOrDate(enum_field_types t)
{
// *not* MYSQL_TYPE_TIME because its range is bigger than QTime
// (see above)
@@ -284,6 +325,11 @@ static bool qIsInteger(int t)
|| t == QMetaType::LongLong || t == QMetaType::ULongLong;
}
+static inline bool qIsBitfield(enum_field_types type)
+{
+ return type == MYSQL_TYPE_BIT;
+}
+
void QMYSQLResultPrivate::bindBlobs()
{
for (int i = 0; i < fields.size(); ++i) {
@@ -328,7 +374,7 @@ bool QMYSQLResultPrivate::bindInValues()
bind->buffer_length = f.bufLength = 0;
hasBlobs = true;
} else if (qIsTimeOrDate(fieldInfo->type)) {
- bind->buffer_length = f.bufLength = sizeof(MYSQL_TIME);
+ bind->buffer_length = f.bufLength = sizeof(QT_MYSQL_TIME);
} else if (qIsInteger(f.type.id())) {
bind->buffer_length = f.bufLength = 8;
} else {
@@ -382,7 +428,7 @@ void QMYSQLResult::cleanup()
if (d->stmt) {
if (mysql_stmt_close(d->stmt))
- qWarning("QMYSQLResult::cleanup: unable to free statement handle");
+ qCWarning(lcMysql, "QMYSQLResult::cleanup: unable to free statement handle");
d->stmt = 0;
}
@@ -391,9 +437,8 @@ void QMYSQLResult::cleanup()
d->meta = 0;
}
- int i;
- for (i = 0; i < d->fields.size(); ++i)
- delete[] d->fields[i].outField;
+ for (const QMYSQLResultPrivate::QMyField &f : std::as_const(d->fields))
+ delete[] f.outField;
if (d->outBinds) {
delete[] d->outBinds;
@@ -483,12 +528,7 @@ bool QMYSQLResult::fetchLast()
return success;
}
- my_ulonglong numRows;
- if (d->preparedQuery) {
- numRows = mysql_stmt_num_rows(d->stmt);
- } else {
- numRows = mysql_num_rows(d->result);
- }
+ my_ulonglong numRows = d->preparedQuery ? mysql_stmt_num_rows(d->stmt) : mysql_num_rows(d->result);
if (at() == int(numRows))
return true;
if (!numRows)
@@ -506,11 +546,25 @@ bool QMYSQLResult::fetchFirst()
return fetch(0);
}
+static inline uint64_t
+qDecodeBitfield(const QMYSQLResultPrivate::QMyField &f, const char *outField)
+{
+ // byte-aligned length
+ const auto numBytes = (f.myField->length + 7) / 8;
+ uint64_t val = 0;
+ for (unsigned long i = 0; i < numBytes && outField; ++i) {
+ uint64_t tmp = static_cast<uint8_t>(outField[i]);
+ val <<= 8;
+ val |= tmp;
+ }
+ return val;
+}
+
QVariant QMYSQLResult::data(int field)
{
Q_D(QMYSQLResult);
if (!isSelect() || field >= d->fields.size()) {
- qWarning("QMYSQLResult::data: column %d out of range", field);
+ qCWarning(lcMysql, "QMYSQLResult::data: column %d out of range", field);
return QVariant();
}
@@ -523,8 +577,9 @@ QVariant QMYSQLResult::data(int field)
if (d->preparedQuery) {
if (f.nullIndicator)
return QVariant(f.type);
-
- if (qIsInteger(f.type.id())) {
+ if (qIsBitfield(f.myField->type)) {
+ return QVariant::fromValue(qDecodeBitfield(f, f.outField));
+ } else if (qIsInteger(f.type.id())) {
QVariant variant(f.type, f.outField);
// we never want to return char variants here, see QTBUG-53397
if (f.type.id() == QMetaType::UChar)
@@ -532,8 +587,8 @@ QVariant QMYSQLResult::data(int field)
else if (f.type.id() == QMetaType::Char)
return variant.toInt();
return variant;
- } else if (qIsTimeOrDate(f.myField->type) && f.bufLength == sizeof(MYSQL_TIME)) {
- auto t = reinterpret_cast<const MYSQL_TIME *>(f.outField);
+ } else if (qIsTimeOrDate(f.myField->type) && f.bufLength >= sizeof(QT_MYSQL_TIME)) {
+ auto t = reinterpret_cast<const QT_MYSQL_TIME *>(f.outField);
QDate date;
QTime time;
if (f.type.id() != QMetaType::QTime)
@@ -541,7 +596,7 @@ QVariant QMYSQLResult::data(int field)
if (f.type.id() != QMetaType::QDate)
time = QTime(t->hour, t->minute, t->second, t->second_part / 1000);
if (f.type.id() == QMetaType::QDateTime)
- return QDateTime(date, time);
+ return QDateTime(date, time, QTimeZone::UTC);
else if (f.type.id() == QMetaType::QDate)
return date;
else
@@ -556,6 +611,9 @@ QVariant QMYSQLResult::data(int field)
return QVariant(f.type);
}
+ if (qIsBitfield(f.myField->type))
+ return QVariant::fromValue(qDecodeBitfield(f, d->row[field]));
+
fieldLength = mysql_fetch_lengths(d->result)[field];
if (f.type.id() != QMetaType::QByteArray)
@@ -664,6 +722,7 @@ bool QMYSQLResult::reset (const QString& query)
for(int i = 0; i < numFields; i++) {
MYSQL_FIELD* field = mysql_fetch_field_direct(d->result, i);
d->fields[i].type = qDecodeMYSQLType(field->type, field->flags);
+ d->fields[i].myField = field;
}
setAt(QSql::BeforeFirstRow);
}
@@ -729,7 +788,7 @@ QSqlRecord QMYSQLResult::record() const
if (!mysql_errno(d->drv_d_func()->mysql)) {
mysql_field_seek(res, 0);
MYSQL_FIELD* field = mysql_fetch_field(res);
- while(field) {
+ while (field) {
info.append(qToField(field));
field = mysql_fetch_field(res);
}
@@ -752,8 +811,8 @@ bool QMYSQLResult::nextResult()
d->result = 0;
setSelect(false);
- for (int i = 0; i < d->fields.size(); ++i)
- delete[] d->fields[i].outField;
+ for (const QMYSQLResultPrivate::QMyField &f : std::as_const(d->fields))
+ delete[] f.outField;
d->fields.clear();
int status = mysql_next_result(d->drv_d_func()->mysql);
@@ -766,7 +825,7 @@ bool QMYSQLResult::nextResult()
}
d->result = mysql_store_result(d->drv_d_func()->mysql);
- int numFields = mysql_field_count(d->drv_d_func()->mysql);
+ unsigned int numFields = mysql_field_count(d->drv_d_func()->mysql);
if (!d->result && numFields > 0) {
setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to store next result"),
QSqlError::StatementError, d->drv_d_func()));
@@ -778,9 +837,10 @@ bool QMYSQLResult::nextResult()
d->rowsAffected = mysql_affected_rows(d->drv_d_func()->mysql);
if (isSelect()) {
- for (int i = 0; i < numFields; i++) {
- MYSQL_FIELD* field = mysql_fetch_field_direct(d->result, i);
+ for (unsigned int i = 0; i < numFields; i++) {
+ MYSQL_FIELD *field = mysql_fetch_field_direct(d->result, i);
d->fields[i].type = qDecodeMYSQLType(field->type, field->flags);
+ d->fields[i].myField = field;
}
}
@@ -793,29 +853,6 @@ void QMYSQLResult::virtual_hook(int id, void *data)
QSqlResult::virtual_hook(id, data);
}
-static MYSQL_TIME *toMySqlDate(QDate date, QTime time, int type)
-{
- Q_ASSERT(type == QMetaType::QTime || type == QMetaType::QDate
- || type == QMetaType::QDateTime);
-
- MYSQL_TIME *myTime = new MYSQL_TIME;
- memset(myTime, 0, sizeof(MYSQL_TIME));
-
- if (type == QMetaType::QTime || type == QMetaType::QDateTime) {
- myTime->hour = time.hour();
- myTime->minute = time.minute();
- myTime->second = time.second();
- myTime->second_part = time.msec() * 1000;
- }
- if (type == QMetaType::QDate || type == QMetaType::QDateTime) {
- myTime->year = date.year();
- myTime->month = date.month();
- myTime->day = date.day();
- }
-
- return myTime;
-}
-
bool QMYSQLResult::prepare(const QString& query)
{
Q_D(QMYSQLResult);
@@ -848,9 +885,9 @@ bool QMYSQLResult::prepare(const QString& query)
return false;
}
- if (mysql_stmt_param_count(d->stmt) > 0) {// allocate memory for outvalues
- d->outBinds = new MYSQL_BIND[mysql_stmt_param_count(d->stmt)];
- }
+ const auto paramCount = mysql_stmt_param_count(d->stmt);
+ if (paramCount > 0) // allocate memory for outvalues
+ d->outBinds = new MYSQL_BIND[paramCount]();
setSelect(d->bindInValues());
d->preparedQuery = true;
@@ -868,7 +905,7 @@ bool QMYSQLResult::exec()
return false;
int r = 0;
- QList<MYSQL_TIME *> timeVector;
+ QList<QT_MYSQL_TIME *> timeVector;
QList<QByteArray> stringVector;
QList<my_bool> nullVector;
@@ -881,11 +918,10 @@ bool QMYSQLResult::exec()
return false;
}
- if (mysql_stmt_param_count(d->stmt) > 0 &&
- mysql_stmt_param_count(d->stmt) == (uint)values.size()) {
-
+ const unsigned long paramCount = mysql_stmt_param_count(d->stmt);
+ if (paramCount > 0 && paramCount == static_cast<size_t>(values.size())) {
nullVector.resize(values.size());
- for (int i = 0; i < values.size(); ++i) {
+ for (qsizetype i = 0; i < values.size(); ++i) {
const QVariant &val = boundValues().at(i);
void *data = const_cast<void *>(val.constData());
@@ -906,27 +942,41 @@ bool QMYSQLResult::exec()
case QMetaType::QTime:
case QMetaType::QDate:
case QMetaType::QDateTime: {
- MYSQL_TIME *myTime = toMySqlDate(val.toDate(), val.toTime(), val.userType());
+ auto myTime = new QT_MYSQL_TIME{};
timeVector.append(myTime);
-
currBind->buffer = myTime;
- switch (val.userType()) {
- case QMetaType::QTime:
+
+ QDate date;
+ QTime time;
+ int type = val.userType();
+ if (type == QMetaType::QTime) {
+ time = val.toTime();
currBind->buffer_type = MYSQL_TYPE_TIME;
myTime->time_type = MYSQL_TIMESTAMP_TIME;
- break;
- case QMetaType::QDate:
+ } else if (type == QMetaType::QDate) {
+ date = val.toDate();
currBind->buffer_type = MYSQL_TYPE_DATE;
myTime->time_type = MYSQL_TIMESTAMP_DATE;
- break;
- case QMetaType::QDateTime:
+ } else {
+ QDateTime dt = val.toDateTime().toUTC();
+ date = dt.date();
+ time = dt.time();
currBind->buffer_type = MYSQL_TYPE_DATETIME;
myTime->time_type = MYSQL_TIMESTAMP_DATETIME;
- break;
- default:
- break;
}
- currBind->buffer_length = sizeof(MYSQL_TIME);
+
+ if (type == QMetaType::QTime || type == QMetaType::QDateTime) {
+ myTime->hour = time.hour();
+ myTime->minute = time.minute();
+ myTime->second = time.second();
+ myTime->second_part = time.msec() * 1000;
+ }
+ if (type == QMetaType::QDate || type == QMetaType::QDateTime) {
+ myTime->year = date.year();
+ myTime->month = date.month();
+ myTime->day = date.day();
+ }
+ currBind->buffer_length = sizeof(QT_MYSQL_TIME);
currBind->length = 0;
break; }
case QMetaType::UInt:
@@ -965,7 +1015,11 @@ bool QMYSQLResult::exec()
}
}
+#if defined(MARIADB_VERSION_ID) || MYSQL_VERSION_ID < 80300
r = mysql_stmt_bind_param(d->stmt, d->outBinds);
+#else
+ r = mysql_stmt_bind_named_param(d->stmt, d->outBinds, paramCount, nullptr);
+#endif
if (r != 0) {
setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
"Unable to bind value"), QSqlError::StatementError, d->stmt));
@@ -1036,7 +1090,7 @@ static void qLibraryInit()
return;
if (mysql_library_init(0, 0, 0)) {
- qWarning("QMYSQLDriver::qServerInit: unable to start server.");
+ qCWarning(lcMysql, "QMYSQLDriver::qServerInit: unable to start server.");
}
#endif // Q_NO_MYSQL_EMBEDDED
@@ -1128,7 +1182,7 @@ bool QMYSQLDriver::hasFeature(DriverFeature f) const
return false;
}
-static void setOptionFlag(uint &optionFlags, const QString &opt)
+static void setOptionFlag(uint &optionFlags, QStringView opt)
{
if (opt == "CLIENT_COMPRESS"_L1)
optionFlags |= CLIENT_COMPRESS;
@@ -1143,85 +1197,167 @@ static void setOptionFlag(uint &optionFlags, const QString &opt)
else if (opt == "CLIENT_ODBC"_L1)
optionFlags |= CLIENT_ODBC;
else if (opt == "CLIENT_SSL"_L1)
- qWarning("QMYSQLDriver: SSL_KEY, SSL_CERT and SSL_CA should be used instead of CLIENT_SSL.");
+ qCWarning(lcMysql, "QMYSQLDriver: MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT "
+ "and MYSQL_OPT_SSL_CA should be used instead of CLIENT_SSL.");
else
- qWarning("QMYSQLDriver::open: Unknown connect option '%s'", opt.toLocal8Bit().constData());
+ qCWarning(lcMysql, "QMYSQLDriver::open: Unknown connect option '%ls'",
+ qUtf16Printable(QString(opt)));
+}
+
+static bool setOptionString(MYSQL *mysql, mysql_option option, QStringView v)
+{
+ return mysql_options(mysql, option, v.toUtf8().constData()) == 0;
}
-bool QMYSQLDriver::open(const QString& db,
- const QString& user,
- const QString& password,
- const QString& host,
- int port,
- const QString& connOpts)
+static bool setOptionInt(MYSQL *mysql, mysql_option option, QStringView v)
+{
+ bool bOk;
+ const auto val = v.toInt(&bOk);
+ return bOk ? mysql_options(mysql, option, &val) == 0 : false;
+}
+
+static bool setOptionBool(MYSQL *mysql, mysql_option option, QStringView v)
+{
+ bool val = (v.isEmpty() || v == "TRUE"_L1 || v == "1"_L1);
+ return mysql_options(mysql, option, &val) == 0;
+}
+
+// MYSQL_OPT_SSL_MODE was introduced with MySQL 5.7.11
+#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID)
+static bool setOptionSslMode(MYSQL *mysql, mysql_option option, QStringView v)
+{
+ mysql_ssl_mode sslMode = SSL_MODE_DISABLED;
+ if (v == "DISABLED"_L1 || v == "SSL_MODE_DISABLED"_L1)
+ sslMode = SSL_MODE_DISABLED;
+ else if (v == "PREFERRED"_L1 || v == "SSL_MODE_PREFERRED"_L1)
+ sslMode = SSL_MODE_PREFERRED;
+ else if (v == "REQUIRED"_L1 || v == "SSL_MODE_REQUIRED"_L1)
+ sslMode = SSL_MODE_REQUIRED;
+ else if (v == "VERIFY_CA"_L1 || v == "SSL_MODE_VERIFY_CA"_L1)
+ sslMode = SSL_MODE_VERIFY_CA;
+ else if (v == "VERIFY_IDENTITY"_L1 || v == "SSL_MODE_VERIFY_IDENTITY"_L1)
+ sslMode = SSL_MODE_VERIFY_IDENTITY;
+ else
+ qCWarning(lcMysql, "Unknown ssl mode '%ls' - using SSL_MODE_DISABLED",
+ qUtf16Printable(QString(v)));
+ return mysql_options(mysql, option, &sslMode) == 0;
+}
+#endif
+
+static bool setOptionProtocol(MYSQL *mysql, mysql_option option, QStringView v)
+{
+ mysql_protocol_type proto = MYSQL_PROTOCOL_DEFAULT;
+ if (v == "TCP"_L1 || v == "MYSQL_PROTOCOL_TCP"_L1)
+ proto = MYSQL_PROTOCOL_TCP;
+ else if (v == "SOCKET"_L1 || v == "MYSQL_PROTOCOL_SOCKET"_L1)
+ proto = MYSQL_PROTOCOL_SOCKET;
+ else if (v == "PIPE"_L1 || v == "MYSQL_PROTOCOL_PIPE"_L1)
+ proto = MYSQL_PROTOCOL_PIPE;
+ else if (v == "MEMORY"_L1 || v == "MYSQL_PROTOCOL_MEMORY"_L1)
+ proto = MYSQL_PROTOCOL_MEMORY;
+ else if (v == "DEFAULT"_L1 || v == "MYSQL_PROTOCOL_DEFAULT"_L1)
+ proto = MYSQL_PROTOCOL_DEFAULT;
+ else
+ qCWarning(lcMysql, "Unknown protocol '%ls' - using MYSQL_PROTOCOL_DEFAULT",
+ qUtf16Printable(QString(v)));
+ return mysql_options(mysql, option, &proto) == 0;
+}
+
+bool QMYSQLDriver::open(const QString &db,
+ const QString &user,
+ const QString &password,
+ const QString &host,
+ int port,
+ const QString &connOpts)
{
Q_D(QMYSQLDriver);
if (isOpen())
close();
+ if (!(d->mysql = mysql_init(nullptr))) {
+ setLastError(qMakeError(tr("Unable to allocate a MYSQL object"),
+ QSqlError::ConnectionError, d));
+ setOpenError(true);
+ return false;
+ }
+
+ typedef bool (*SetOptionFunc)(MYSQL*, mysql_option, QStringView);
+ struct mysqloptions {
+ QLatin1StringView key;
+ mysql_option option;
+ SetOptionFunc func;
+ };
+ const mysqloptions options[] = {
+ {"SSL_KEY"_L1, MYSQL_OPT_SSL_KEY, setOptionString},
+ {"SSL_CERT"_L1, MYSQL_OPT_SSL_CERT, setOptionString},
+ {"SSL_CA"_L1, MYSQL_OPT_SSL_CA, setOptionString},
+ {"SSL_CAPATH"_L1, MYSQL_OPT_SSL_CAPATH, setOptionString},
+ {"SSL_CIPHER"_L1, MYSQL_OPT_SSL_CIPHER, setOptionString},
+ {"MYSQL_OPT_SSL_KEY"_L1, MYSQL_OPT_SSL_KEY, setOptionString},
+ {"MYSQL_OPT_SSL_CERT"_L1, MYSQL_OPT_SSL_CERT, setOptionString},
+ {"MYSQL_OPT_SSL_CA"_L1, MYSQL_OPT_SSL_CA, setOptionString},
+ {"MYSQL_OPT_SSL_CAPATH"_L1, MYSQL_OPT_SSL_CAPATH, setOptionString},
+ {"MYSQL_OPT_SSL_CIPHER"_L1, MYSQL_OPT_SSL_CIPHER, setOptionString},
+ {"MYSQL_OPT_SSL_CRL"_L1, MYSQL_OPT_SSL_CRL, setOptionString},
+ {"MYSQL_OPT_SSL_CRLPATH"_L1, MYSQL_OPT_SSL_CRLPATH, setOptionString},
+#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50710
+ {"MYSQL_OPT_TLS_VERSION"_L1, MYSQL_OPT_TLS_VERSION, setOptionString},
+#endif
+#if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 50711 && !defined(MARIADB_VERSION_ID)
+ {"MYSQL_OPT_SSL_MODE"_L1, MYSQL_OPT_SSL_MODE, setOptionSslMode},
+#endif
+ {"MYSQL_OPT_CONNECT_TIMEOUT"_L1, MYSQL_OPT_CONNECT_TIMEOUT, setOptionInt},
+ {"MYSQL_OPT_READ_TIMEOUT"_L1, MYSQL_OPT_READ_TIMEOUT, setOptionInt},
+ {"MYSQL_OPT_WRITE_TIMEOUT"_L1, MYSQL_OPT_WRITE_TIMEOUT, setOptionInt},
+ {"MYSQL_OPT_RECONNECT"_L1, MYSQL_OPT_RECONNECT, setOptionBool},
+ {"MYSQL_OPT_LOCAL_INFILE"_L1, MYSQL_OPT_LOCAL_INFILE, setOptionInt},
+ {"MYSQL_OPT_PROTOCOL"_L1, MYSQL_OPT_PROTOCOL, setOptionProtocol},
+ {"MYSQL_SHARED_MEMORY_BASE_NAME"_L1, MYSQL_SHARED_MEMORY_BASE_NAME, setOptionString},
+ };
+ auto trySetOption = [&](const QStringView &key, const QStringView &value) -> bool {
+ for (const mysqloptions &opt : options) {
+ if (key == opt.key) {
+ if (!opt.func(d->mysql, opt.option, value)) {
+ qCWarning(lcMysql, "QMYSQLDriver::open: Could not set connect option value "
+ "'%ls' to '%ls'",
+ qUtf16Printable(QString(key)), qUtf16Printable(QString(value)));
+ }
+ return true;
+ }
+ }
+ return false;
+ };
+
/* This is a hack to get MySQL's stored procedure support working.
Since a stored procedure _may_ return multiple result sets,
we have to enable CLIEN_MULTI_STATEMENTS here, otherwise _any_
stored procedure call will fail.
*/
unsigned int optionFlags = CLIENT_MULTI_STATEMENTS;
- const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts));
+ const QList<QStringView> opts(QStringView(connOpts).split(u';', Qt::SkipEmptyParts));
QString unixSocket;
- QString sslCert;
- QString sslCA;
- QString sslKey;
- QString sslCAPath;
- QString sslCipher;
- my_bool reconnect=false;
- uint connectTimeout = 0;
- uint readTimeout = 0;
- uint writeTimeout = 0;
// extract the real options from the string
- for (int i = 0; i < opts.count(); ++i) {
- QString tmp(opts.at(i).simplified());
+ for (const auto &option : opts) {
+ const QStringView sv = QStringView(option).trimmed();
qsizetype idx;
- if ((idx = tmp.indexOf(u'=')) != -1) {
- QString val = tmp.mid(idx + 1).simplified();
- QString opt = tmp.left(idx).simplified();
- if (opt == "UNIX_SOCKET"_L1)
- unixSocket = val;
- else if (opt == "MYSQL_OPT_RECONNECT"_L1) {
- if (val == "TRUE"_L1 || val == "1"_L1 || val.isEmpty())
- reconnect = true;
- } else if (opt == "MYSQL_OPT_CONNECT_TIMEOUT"_L1)
- connectTimeout = val.toInt();
- else if (opt == "MYSQL_OPT_READ_TIMEOUT"_L1)
- readTimeout = val.toInt();
- else if (opt == "MYSQL_OPT_WRITE_TIMEOUT"_L1)
- writeTimeout = val.toInt();
- else if (opt == "SSL_KEY"_L1)
- sslKey = val;
- else if (opt == "SSL_CERT"_L1)
- sslCert = val;
- else if (opt == "SSL_CA"_L1)
- sslCA = val;
- else if (opt == "SSL_CAPATH"_L1)
- sslCAPath = val;
- else if (opt == "SSL_CIPHER"_L1)
- sslCipher = val;
+ if ((idx = sv.indexOf(u'=')) != -1) {
+ const QStringView key = sv.left(idx).trimmed();
+ const QStringView val = sv.mid(idx + 1).trimmed();
+ if (trySetOption(key, val))
+ continue;
+ else if (key == "UNIX_SOCKET"_L1)
+ unixSocket = val.toString();
else if (val == "TRUE"_L1 || val == "1"_L1)
- setOptionFlag(optionFlags, tmp.left(idx).simplified());
+ setOptionFlag(optionFlags, key);
else
- qWarning("QMYSQLDriver::open: Illegal connect option value '%s'",
- tmp.toLocal8Bit().constData());
+ qCWarning(lcMysql, "QMYSQLDriver::open: Illegal connect option value '%ls'",
+ qUtf16Printable(QString(sv)));
} else {
- setOptionFlag(optionFlags, tmp);
+ setOptionFlag(optionFlags, sv);
}
}
- if (!(d->mysql = mysql_init(nullptr))) {
- setLastError(qMakeError(tr("Unable to allocate a MYSQL object"),
- QSqlError::ConnectionError, d));
- setOpenError(true);
- return false;
- }
-
// try utf8 with non BMP first, utf8 (BMP only) if that fails
static const char wanted_charsets[][8] = { "utf8mb4", "utf8" };
#ifdef MARIADB_VERSION_ID
@@ -1240,23 +1376,6 @@ bool QMYSQLDriver::open(const QString& db,
} *cs = nullptr;
#endif
- if (!sslKey.isNull() || !sslCert.isNull() || !sslCA.isNull() ||
- !sslCAPath.isNull() || !sslCipher.isNull()) {
- mysql_ssl_set(d->mysql,
- sslKey.isNull() ? nullptr : sslKey.toUtf8().constData(),
- sslCert.isNull() ? nullptr : sslCert.toUtf8().constData(),
- sslCA.isNull() ? nullptr : sslCA.toUtf8().constData(),
- sslCAPath.isNull() ? nullptr : sslCAPath.toUtf8().constData(),
- sslCipher.isNull() ? nullptr : sslCipher.toUtf8().constData());
- }
-
- if (connectTimeout != 0)
- mysql_options(d->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &connectTimeout);
- if (readTimeout != 0)
- mysql_options(d->mysql, MYSQL_OPT_READ_TIMEOUT, &readTimeout);
- if (writeTimeout != 0)
- mysql_options(d->mysql, MYSQL_OPT_WRITE_TIMEOUT, &writeTimeout);
-
MYSQL *mysql = mysql_real_connect(d->mysql,
host.isNull() ? nullptr : host.toUtf8().constData(),
user.isNull() ? nullptr : user.toUtf8().constData(),
@@ -1285,9 +1404,10 @@ bool QMYSQLDriver::open(const QString& db,
}
}
if (!ok)
- qWarning("MySQL: Unable to set the client character set to utf8 (\"%s\"). Using '%s' instead.",
- mysql_error(d->mysql),
- mysql_character_set_name(d->mysql));
+ qCWarning(lcMysql, "MySQL: Unable to set the client character set to utf8 (\"%s\"). "
+ "Using '%s' instead.",
+ mysql_error(d->mysql),
+ mysql_character_set_name(d->mysql));
}
if (!db.isEmpty() && mysql_select_db(d->mysql, db.toUtf8().constData())) {
@@ -1297,10 +1417,11 @@ bool QMYSQLDriver::open(const QString& db,
return false;
}
- if (reconnect)
- mysql_options(d->mysql, MYSQL_OPT_RECONNECT, &reconnect);
-
d->preparedQuerysEnabled = checkPreparedQueries(d->mysql);
+ d->dbName = db;
+
+ if (d->preparedQuerysEnabled)
+ setUtcTimeZone(d->mysql);
#if QT_CONFIG(thread)
mysql_thread_init();
@@ -1320,6 +1441,7 @@ void QMYSQLDriver::close()
#endif
mysql_close(d->mysql);
d->mysql = nullptr;
+ d->dbName.clear();
setOpen(false);
setOpenError(false);
}
@@ -1336,14 +1458,14 @@ QStringList QMYSQLDriver::tables(QSql::TableType type) const
QStringList tl;
QSqlQuery q(createResult());
if (type & QSql::Tables) {
- QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + QLatin1StringView(d->mysql->db) + "' and table_type = 'BASE TABLE'"_L1;
+ QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'BASE TABLE'"_L1;
q.exec(sql);
while (q.next())
tl.append(q.value(0).toString());
}
if (type & QSql::Views) {
- QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + QLatin1StringView(d->mysql->db) + "' and table_type = 'VIEW'"_L1;
+ QString sql = "select table_name from information_schema.tables where table_schema = '"_L1 + d->dbName + "' and table_type = 'VIEW'"_L1;
q.exec(sql);
while (q.next())
@@ -1352,7 +1474,7 @@ QStringList QMYSQLDriver::tables(QSql::TableType type) const
return tl;
}
-QSqlIndex QMYSQLDriver::primaryIndex(const QString& tablename) const
+QSqlIndex QMYSQLDriver::primaryIndex(const QString &tablename) const
{
QSqlIndex idx;
if (!isOpen())
@@ -1373,26 +1495,41 @@ QSqlIndex QMYSQLDriver::primaryIndex(const QString& tablename) const
return idx;
}
-QSqlRecord QMYSQLDriver::record(const QString& tablename) const
+QSqlRecord QMYSQLDriver::record(const QString &tablename) const
{
Q_D(const QMYSQLDriver);
- QString table=tablename;
- if (isIdentifierEscaped(table, QSqlDriver::TableName))
- table = stripDelimiters(table, QSqlDriver::TableName);
-
- QSqlRecord info;
if (!isOpen())
- return info;
- MYSQL_RES* r = mysql_list_fields(d->mysql, table.toUtf8().constData(), 0);
- if (!r) {
- return info;
+ return {};
+ QSqlQuery i(createResult());
+ QString stmt("SELECT * FROM %1 LIMIT 0"_L1);
+ i.exec(stmt.arg(escapeIdentifier(tablename, QSqlDriver::TableName)));
+ auto r = i.record();
+ if (r.isEmpty())
+ return r;
+ // no binding of WHERE possible with MySQL
+ // escaping on WHERE clause does not work, so use mysql_real_escape_string()
+ stmt = "SELECT column_name, column_default FROM information_schema.columns WHERE table_name = '%1'"_L1;
+ const auto baTableName = tablename.toUtf8();
+ QVarLengthArray<char> tableNameQuoted(baTableName.size() * 2 + 1);
+#if defined(MARIADB_VERSION_ID)
+ const auto len = mysql_real_escape_string(d->mysql, tableNameQuoted.data(),
+ baTableName.data(), baTableName.size());
+#else
+ const auto len = mysql_real_escape_string_quote(d->mysql, tableNameQuoted.data(),
+ baTableName.data(), baTableName.size(), '\'');
+#endif
+ if (i.exec(stmt.arg(QString::fromUtf8(tableNameQuoted.data(), len)))) {
+ while (i.next()) {
+ const auto colName = i.value(0).toString();
+ const auto recordIdx = r.indexOf(colName);
+ if (recordIdx >= 0) {
+ auto field = r.field(recordIdx);
+ field.setDefaultValue(i.value(1));
+ r.replace(recordIdx, field);
+ }
+ }
}
- MYSQL_FIELD* field;
-
- while ((field = mysql_fetch_field(r)))
- info.append(qToField(field));
- mysql_free_result(r);
- return info;
+ return r;
}
QVariant QMYSQLDriver::handle() const
@@ -1405,7 +1542,7 @@ bool QMYSQLDriver::beginTransaction()
{
Q_D(QMYSQLDriver);
if (!isOpen()) {
- qWarning("QMYSQLDriver::beginTransaction: Database not open");
+ qCWarning(lcMysql, "QMYSQLDriver::beginTransaction: Database not open");
return false;
}
if (mysql_query(d->mysql, "BEGIN WORK")) {
@@ -1420,7 +1557,7 @@ bool QMYSQLDriver::commitTransaction()
{
Q_D(QMYSQLDriver);
if (!isOpen()) {
- qWarning("QMYSQLDriver::commitTransaction: Database not open");
+ qCWarning(lcMysql, "QMYSQLDriver::commitTransaction: Database not open");
return false;
}
if (mysql_query(d->mysql, "COMMIT")) {
@@ -1435,7 +1572,7 @@ bool QMYSQLDriver::rollbackTransaction()
{
Q_D(QMYSQLDriver);
if (!isOpen()) {
- qWarning("QMYSQLDriver::rollbackTransaction: Database not open");
+ qCWarning(lcMysql, "QMYSQLDriver::rollbackTransaction: Database not open");
return false;
}
if (mysql_query(d->mysql, "ROLLBACK")) {
@@ -1472,7 +1609,7 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons
r = u'\'' + QString::fromUtf8(buffer.data(), escapedSize) + u'\'';
break;
} else {
- qWarning("QMYSQLDriver::formatValue: Database not open");
+ qCWarning(lcMysql, "QMYSQLDriver::formatValue: Database not open");
}
Q_FALLTHROUGH();
case QMetaType::QDateTime:
@@ -1481,7 +1618,6 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons
// "+00:00" starting in version 8.0.19. However, if we got here,
// it's because the MySQL server is too old for prepared queries
// in the first place, so it won't understand timezones either.
- // Besides, MYSQL_TIME does not support timezones, so match it.
r = u'\'' +
dt.date().toString(Qt::ISODate) +
u'T' +
@@ -1499,9 +1635,9 @@ QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) cons
QString QMYSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) const
{
QString res = identifier;
- if (!identifier.isEmpty() && !identifier.startsWith(u'`') && !identifier.endsWith(u'`') ) {
- res.prepend(u'`').append(u'`');
+ if (!identifier.isEmpty() && !identifier.startsWith(u'`') && !identifier.endsWith(u'`')) {
res.replace(u'.', "`.`"_L1);
+ res = u'`' + res + u'`';
}
return res;
}