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.cpp184
1 files changed, 113 insertions, 71 deletions
diff --git a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp
index d5a3cd096f..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>
@@ -51,6 +53,8 @@ struct QT_MYSQL_TIME
QT_BEGIN_NAMESPACE
+static Q_LOGGING_CATEGORY(lcMysql, "qt.sql.mysql")
+
using namespace Qt::StringLiterals;
class QMYSQLDriverPrivate : public QSqlDriverPrivate
@@ -97,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
}
@@ -118,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
@@ -266,7 +288,6 @@ static QSqlField qToField(MYSQL_FIELD *field)
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;
}
@@ -407,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;
}
@@ -543,7 +564,7 @@ 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();
}
@@ -575,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
@@ -832,28 +853,6 @@ void QMYSQLResult::virtual_hook(int id, void *data)
QSqlResult::virtual_hook(id, data);
}
-static QT_MYSQL_TIME *toMySqlDate(QDate date, QTime time, int type)
-{
- Q_ASSERT(type == QMetaType::QTime || type == QMetaType::QDate
- || type == QMetaType::QDateTime);
-
- auto myTime = new QT_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);
@@ -919,9 +918,8 @@ 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 (qsizetype i = 0; i < values.size(); ++i) {
const QVariant &val = boundValues().at(i);
@@ -944,25 +942,39 @@ bool QMYSQLResult::exec()
case QMetaType::QTime:
case QMetaType::QDate:
case QMetaType::QDateTime: {
- QT_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;
+ }
+
+ 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;
@@ -1003,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));
@@ -1074,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
@@ -1181,9 +1197,11 @@ static void setOptionFlag(uint &optionFlags, QStringView opt)
else if (opt == "CLIENT_ODBC"_L1)
optionFlags |= CLIENT_ODBC;
else if (opt == "CLIENT_SSL"_L1)
- qWarning("QMYSQLDriver: MYSQL_OPT_SSL_KEY, MYSQL_OPT_SSL_CERT and MYSQL_OPT_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)
@@ -1220,7 +1238,8 @@ static bool setOptionSslMode(MYSQL *mysql, mysql_option option, QStringView v)
else if (v == "VERIFY_IDENTITY"_L1 || v == "SSL_MODE_VERIFY_IDENTITY"_L1)
sslMode = SSL_MODE_VERIFY_IDENTITY;
else
- qWarning() << "Unknown ssl mode '" << v << "' - using SSL_MODE_DISABLED";
+ qCWarning(lcMysql, "Unknown ssl mode '%ls' - using SSL_MODE_DISABLED",
+ qUtf16Printable(QString(v)));
return mysql_options(mysql, option, &sslMode) == 0;
}
#endif
@@ -1239,7 +1258,8 @@ static bool setOptionProtocol(MYSQL *mysql, mysql_option option, QStringView v)
else if (v == "DEFAULT"_L1 || v == "MYSQL_PROTOCOL_DEFAULT"_L1)
proto = MYSQL_PROTOCOL_DEFAULT;
else
- qWarning() << "Unknown protocol '" << v << "' - using MYSQL_PROTOCOL_DEFAULT";
+ qCWarning(lcMysql, "Unknown protocol '%ls' - using MYSQL_PROTOCOL_DEFAULT",
+ qUtf16Printable(QString(v)));
return mysql_options(mysql, option, &proto) == 0;
}
@@ -1298,8 +1318,9 @@ bool QMYSQLDriver::open(const QString &db,
for (const mysqloptions &opt : options) {
if (key == opt.key) {
if (!opt.func(d->mysql, opt.option, value)) {
- qWarning("QMYSQLDriver::open: Could not set connect option value '%s' to '%s'",
- key.toLocal8Bit().constData(), value.toLocal8Bit().constData());
+ qCWarning(lcMysql, "QMYSQLDriver::open: Could not set connect option value "
+ "'%ls' to '%ls'",
+ qUtf16Printable(QString(key)), qUtf16Printable(QString(value)));
}
return true;
}
@@ -1330,8 +1351,8 @@ bool QMYSQLDriver::open(const QString &db,
else if (val == "TRUE"_L1 || val == "1"_L1)
setOptionFlag(optionFlags, key);
else
- qWarning("QMYSQLDriver::open: Illegal connect option value '%s'",
- sv.toLocal8Bit().constData());
+ qCWarning(lcMysql, "QMYSQLDriver::open: Illegal connect option value '%ls'",
+ qUtf16Printable(QString(sv)));
} else {
setOptionFlag(optionFlags, sv);
}
@@ -1383,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())) {
@@ -1398,6 +1420,9 @@ bool QMYSQLDriver::open(const QString &db,
d->preparedQuerysEnabled = checkPreparedQueries(d->mysql);
d->dbName = db;
+ if (d->preparedQuerysEnabled)
+ setUtcTimeZone(d->mysql);
+
#if QT_CONFIG(thread)
mysql_thread_init();
#endif
@@ -1473,20 +1498,38 @@ QSqlIndex QMYSQLDriver::primaryIndex(const QString &tablename) const
QSqlRecord QMYSQLDriver::record(const QString &tablename) const
{
Q_D(const QMYSQLDriver);
- const QString table = stripDelimiters(tablename, QSqlDriver::TableName);
-
- QSqlRecord info;
if (!isOpen())
- return info;
- MYSQL_RES *r = mysql_list_fields(d->mysql, table.toUtf8().constData(), nullptr);
- if (!r)
- return info;
-
- MYSQL_FIELD *field;
- while ((field = mysql_fetch_field(r)))
- info.append(qToField(field));
- mysql_free_result(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);
+ }
+ }
+ }
+ return r;
}
QVariant QMYSQLDriver::handle() const
@@ -1499,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")) {
@@ -1514,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")) {
@@ -1529,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")) {
@@ -1566,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:
@@ -1575,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' +