diff options
Diffstat (limited to 'src/plugins/sqldrivers')
29 files changed, 3267 insertions, 1018 deletions
diff --git a/src/plugins/sqldrivers/.cmake.conf b/src/plugins/sqldrivers/.cmake.conf index 146dd2bd5a..10bc1fd407 100644 --- a/src/plugins/sqldrivers/.cmake.conf +++ b/src/plugins/sqldrivers/.cmake.conf @@ -1 +1 @@ -set(QT_REPO_MODULE_VERSION "6.6.0") +set(QT_REPO_MODULE_VERSION "6.8.0") diff --git a/src/plugins/sqldrivers/CMakeLists.txt b/src/plugins/sqldrivers/CMakeLists.txt index 932e501c48..43abb00ad1 100644 --- a/src/plugins/sqldrivers/CMakeLists.txt +++ b/src/plugins/sqldrivers/CMakeLists.txt @@ -1,12 +1,17 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from sqldrivers.pro. - -# special case begin cmake_minimum_required(VERSION 3.16) if (NOT CMAKE_PROJECT_NAME STREQUAL "QtBase" AND NOT CMAKE_PROJECT_NAME STREQUAL "Qt") include(.cmake.conf) + # Store initial build type (if any is specified) to be read by + # qt_internal_set_cmake_build_type(). + # See qt_internal_set_cmake_build_type() for details. + if(DEFINED CACHE{CMAKE_BUILD_TYPE}) + set(__qt_internal_standalone_project_cmake_build_type_before_project_call + "${CMAKE_BUILD_TYPE}") + endif() + project(QSQLiteDriverPlugins VERSION "${QT_REPO_MODULE_VERSION}" DESCRIPTION "Qt6 SQL driver plugins" @@ -67,7 +72,10 @@ if(QT_FEATURE_sql_ibase) add_subdirectory(ibase) endif() +if(QT_FEATURE_sql_mimer) + add_subdirectory(mimer) +endif() + if(NOT CMAKE_PROJECT_NAME STREQUAL "QtBase" AND NOT CMAKE_PROJECT_NAME STREQUAL "Qt") qt_print_feature_summary() endif() -# special case end diff --git a/src/plugins/sqldrivers/configure.cmake b/src/plugins/sqldrivers/configure.cmake index 4132196e6c..534ac020d8 100644 --- a/src/plugins/sqldrivers/configure.cmake +++ b/src/plugins/sqldrivers/configure.cmake @@ -20,6 +20,7 @@ qt_find_package(Oracle PROVIDED_TARGETS Oracle::OCI MODULE_NAME sqldrivers QMAKE qt_find_package(ODBC PROVIDED_TARGETS ODBC::ODBC MODULE_NAME sqldrivers QMAKE_LIB odbc) qt_find_package(SQLite3 PROVIDED_TARGETS SQLite::SQLite3 MODULE_NAME sqldrivers QMAKE_LIB sqlite3) qt_find_package(Interbase PROVIDED_TARGETS Interbase::Interbase MODULE_NAME sqldrivers QMAKE_LIB ibase) # special case +qt_find_package(Mimer PROVIDED_TARGETS MimerSQL::MimerSQL MODULE_NAME sqldrivers QMAKE_LIB mimer) if(NOT WIN32 AND QT_FEATURE_system_zlib) qt_add_qmake_lib_dependency(sqlite3 zlib) endif() @@ -64,6 +65,11 @@ qt_feature("system-sqlite" PRIVATE AUTODETECT OFF CONDITION QT_FEATURE_sql_sqlite AND SQLite3_FOUND ) +qt_feature("sql-mimer" PRIVATE + LABEL "Mimer" + CONDITION Mimer_FOUND +) + qt_configure_add_summary_section(NAME "Qt Sql Drivers") qt_configure_add_summary_entry(ARGS "sql-db2") qt_configure_add_summary_entry(ARGS "sql-ibase") @@ -73,6 +79,7 @@ qt_configure_add_summary_entry(ARGS "sql-odbc") qt_configure_add_summary_entry(ARGS "sql-psql") qt_configure_add_summary_entry(ARGS "sql-sqlite") qt_configure_add_summary_entry(ARGS "system-sqlite") +qt_configure_add_summary_entry(ARGS "sql-mimer") qt_configure_end_summary_section() # end of "Qt Sql Drivers" section qt_configure_add_report_entry( TYPE WARNING diff --git a/src/plugins/sqldrivers/db2/CMakeLists.txt b/src/plugins/sqldrivers/db2/CMakeLists.txt index 4f5680799f..119a5b53c9 100644 --- a/src/plugins/sqldrivers/db2/CMakeLists.txt +++ b/src/plugins/sqldrivers/db2/CMakeLists.txt @@ -1,8 +1,6 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from db2.pro. - ##################################################################### ## QDB2DriverPlugin Plugin: ##################################################################### @@ -23,9 +21,6 @@ qt_internal_add_plugin(QDB2DriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:db2.pro:<TRUE>: -# OTHER_FILES = "db2.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/db2/qsql_db2.cpp b/src/plugins/sqldrivers/db2/qsql_db2.cpp index df2b35ecd1..9b6a06c378 100644 --- a/src/plugins/sqldrivers/db2/qsql_db2.cpp +++ b/src/plugins/sqldrivers/db2/qsql_db2.cpp @@ -14,6 +14,7 @@ #include <QDebug> #include <QtSql/private/qsqldriver_p.h> #include <QtSql/private/qsqlresult_p.h> +#include "private/qtools_p.h" #if defined(Q_CC_BOR) // DB2's sqlsystm.h (included through sqlcli1.h) defines the SQL_BIGINT_TYPE @@ -43,10 +44,13 @@ class QDB2DriverPrivate : public QSqlDriverPrivate Q_DECLARE_PUBLIC(QDB2Driver) public: - QDB2DriverPrivate() : QSqlDriverPrivate(), hEnv(0), hDbc(0) { dbmsType = QSqlDriver::DB2; } - SQLHANDLE hEnv; - SQLHANDLE hDbc; + QDB2DriverPrivate() : QSqlDriverPrivate(QSqlDriver::DB2) {} + SQLHANDLE hEnv = 0; + SQLHANDLE hDbc = 0; QString user; + + void qSplitTableQualifier(const QString &qualifier, QString &catalog, + QString &schema, QString &table) const; }; class QDB2ResultPrivate; @@ -301,7 +305,6 @@ static QSqlField qMakeFieldInfo(const QDB2ResultPrivate* d, int i) // else required is unknown f.setLength(colSize == 0 ? -1 : int(colSize)); f.setPrecision(colScale == 0 ? -1 : int(colScale)); - f.setSqlType(int(colType)); SQLTCHAR tableName[TABLENAMESIZE]; SQLSMALLINT tableNameLen; r = SQLColAttribute(d->hStmt, i + 1, SQL_DESC_BASE_TABLE_NAME, tableName, @@ -461,34 +464,27 @@ static QByteArray qGetBinaryData(SQLHANDLE hStmt, int column, SQLLEN& lengthIndi return fieldVal; } -static void qSplitTableQualifier(const QString & qualifier, QString * catalog, - QString * schema, QString * table) +void QDB2DriverPrivate::qSplitTableQualifier(const QString &qualifier, QString &catalog, + QString &schema, QString &table) const { - if (!catalog || !schema || !table) - return; - QStringList l = qualifier.split(u'.'); - if (l.count() > 3) - return; // can't possibly be a valid table qualifier - int i = 0, n = l.count(); - if (n == 1) { - *table = qualifier; - } else { - for (QStringList::Iterator it = l.begin(); it != l.end(); ++it) { - if (n == 3) { - if (i == 0) - *catalog = *it; - else if (i == 1) - *schema = *it; - else if (i == 2) - *table = *it; - } else if (n == 2) { - if (i == 0) - *schema = *it; - else if (i == 1) - *table = *it; - } - i++; - } + const QList<QStringView> l = QStringView(qualifier).split(u'.'); + switch (l.count()) { + case 1: + table = qualifier; + break; + case 2: + schema = l.at(0).toString(); + table = l.at(1).toString(); + break; + case 3: + catalog = l.at(0).toString(); + schema = l.at(1).toString(); + table = l.at(2).toString(); + break; + default: + qSqlWarning(QString::fromLatin1("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'") + .arg(qualifier), this); + break; } } @@ -508,7 +504,6 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt) // else we don't know. f.setLength(qGetIntData(hStmt, 6, isNull)); // column size f.setPrecision(qGetIntData(hStmt, 8, isNull)); // precision - f.setSqlType(type); return f; } @@ -1353,7 +1348,7 @@ QSqlRecord QDB2Driver::record(const QString& tableName) const SQLHANDLE hStmt; QString catalog, schema, table; - qSplitTableQualifier(tableName, &catalog, &schema, &table); + d->qSplitTableQualifier(tableName, catalog, schema, table); if (schema.isEmpty()) schema = d->user; @@ -1509,7 +1504,7 @@ QSqlIndex QDB2Driver::primaryIndex(const QString& tablename) const return index; } QString catalog, schema, table; - qSplitTableQualifier(tablename, &catalog, &schema, &table); + d->qSplitTableQualifier(tablename, catalog, schema, table); if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) catalog = stripDelimiters(catalog, QSqlDriver::TableName); @@ -1674,17 +1669,17 @@ QString QDB2Driver::formatValue(const QSqlField &field, bool trimStrings) const } } case QMetaType::QByteArray: { - QByteArray ba = field.value().toByteArray(); - QString res; - res += "BLOB(X'"_L1; - static const char hexchars[] = "0123456789abcdef"; - for (int i = 0; i < ba.size(); ++i) { - uchar s = (uchar) ba[i]; - res += QLatin1Char(hexchars[s >> 4]); - res += QLatin1Char(hexchars[s & 0x0f]); + const QByteArray ba = field.value().toByteArray(); + QString r; + r.reserve(ba.size() * 2 + 9); + r += "BLOB(X'"_L1; + for (const char c : ba) { + const uchar s = uchar(c); + r += QLatin1Char(QtMiscUtils::toHexLower(s >> 4)); + r += QLatin1Char(QtMiscUtils::toHexLower(s & 0x0f)); } - res += "')"_L1; - return res; + r += "')"_L1; + return r; } default: return QSqlDriver::formatValue(field, trimStrings); @@ -1702,10 +1697,12 @@ QString QDB2Driver::escapeIdentifier(const QString &identifier, IdentifierType) QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } QT_END_NAMESPACE + +#include "moc_qsql_db2_p.cpp" diff --git a/src/plugins/sqldrivers/ibase/CMakeLists.txt b/src/plugins/sqldrivers/ibase/CMakeLists.txt index a21be56b07..b8f2d2561f 100644 --- a/src/plugins/sqldrivers/ibase/CMakeLists.txt +++ b/src/plugins/sqldrivers/ibase/CMakeLists.txt @@ -10,6 +10,7 @@ qt_internal_add_plugin(QIBaseDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES Interbase::Interbase Qt::Core diff --git a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp index d228628c08..28361e250d 100644 --- a/src/plugins/sqldrivers/ibase/qsql_ibase.cpp +++ b/src/plugins/sqldrivers/ibase/qsql_ibase.cpp @@ -4,9 +4,11 @@ #include "qsql_ibase_p.h" #include <QtCore/qcoreapplication.h> #include <QtCore/qdatetime.h> +#include <QtCore/qtimezone.h> #include <QtCore/qdeadlinetimer.h> #include <QtCore/qdebug.h> #include <QtCore/qlist.h> +#include <QtCore/qloggingcategory.h> #include <QtCore/qmap.h> #include <QtCore/qmutex.h> #include <QtCore/qvariant.h> @@ -20,9 +22,12 @@ #include <stdlib.h> #include <limits.h> #include <math.h> +#include <mutex> QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcIbase, "qt.sql.ibase") + using namespace Qt::StringLiterals; #define FBVERSION SQL_DIALECT_V6 @@ -36,7 +41,15 @@ using namespace Qt::StringLiterals; #define blr_boolean_dtype blr_bool #endif -enum { QIBaseChunkSize = SHRT_MAX / 2 }; +constexpr qsizetype QIBaseChunkSize = SHRT_MAX / 2; + +#if (FB_API_VER >= 40) +typedef QMap<quint16, QByteArray> QFbTzIdToIanaIdMap; +typedef QMap<QByteArray, quint16> QIanaIdToFbTzIdMap; +Q_GLOBAL_STATIC(QFbTzIdToIanaIdMap, qFbTzIdToIanaIdMap) +Q_GLOBAL_STATIC(QIanaIdToFbTzIdMap, qIanaIdToFbTzIdMap) +std::once_flag initTZMappingFlag; +#endif static bool getIBaseError(QString& msg, const ISC_STATUS* status, ISC_LONG &sqlcode) { @@ -85,6 +98,9 @@ static void initDA(XSQLDA *sqlda) case SQL_FLOAT: case SQL_DOUBLE: case SQL_TIMESTAMP: +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: +#endif case SQL_TYPE_TIME: case SQL_TYPE_DATE: case SQL_TEXT: @@ -122,7 +138,7 @@ static void delDA(XSQLDA *&sqlda) delete [] sqlda->sqlvar[i].sqldata; } free(sqlda); - sqlda = 0; + sqlda = nullptr; } static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) @@ -139,6 +155,9 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) case blr_sql_date: return QMetaType::QDate; case blr_timestamp: +#if (FB_API_VER >= 40) + case blr_timestamp_tz: +#endif return QMetaType::QDateTime; case blr_blob: return QMetaType::QByteArray; @@ -155,7 +174,7 @@ static QMetaType::Type qIBaseTypeName(int iType, bool hasScale) case blr_boolean_dtype: return QMetaType::Bool; } - qWarning("qIBaseTypeName: unknown datatype: %d", iType); + qCWarning(lcIbase, "qIBaseTypeName: unknown datatype: %d", iType); return QMetaType::UnknownType; } @@ -174,6 +193,9 @@ static QMetaType::Type qIBaseTypeName2(int iType, bool hasScale) case SQL_DOUBLE: return QMetaType::Double; case SQL_TIMESTAMP: +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: +#endif return QMetaType::QDateTime; case SQL_TYPE_TIME: return QMetaType::QTime; @@ -208,12 +230,45 @@ static QDateTime fromTimeStamp(char *buffer) // have to demangle the structure ourselves because isc_decode_time // strips the msecs - t = t.addMSecs(int(((ISC_TIMESTAMP*)buffer)->timestamp_time / 10)); - d = bd.addDays(int(((ISC_TIMESTAMP*)buffer)->timestamp_date)); - + auto timebuf = reinterpret_cast<ISC_TIMESTAMP*>(buffer); + t = t.addMSecs(static_cast<int>(timebuf->timestamp_time / 10)); + d = bd.addDays(timebuf->timestamp_date); return QDateTime(d, t); } +#if (FB_API_VER >= 40) +QDateTime fromTimeStampTz(char *buffer) +{ + static const QDate bd(1858, 11, 17); + QTime t(0, 0); + QDate d; + + // have to demangle the structure ourselves because isc_decode_time + // strips the msecs + auto timebuf = reinterpret_cast<ISC_TIMESTAMP_TZ*>(buffer); + t = t.addMSecs(static_cast<int>(timebuf->utc_timestamp.timestamp_time / 10)); + d = bd.addDays(timebuf->utc_timestamp.timestamp_date); + quint16 fpTzID = timebuf->time_zone; + + QByteArray timeZoneName = qFbTzIdToIanaIdMap()->value(fpTzID); + if (!timeZoneName.isEmpty()) + return QDateTime(d, t, QTimeZone(timeZoneName)); + else + return {}; +} + +ISC_TIMESTAMP_TZ toTimeStampTz(const QDateTime &dt) +{ + static const QTime midnight(0, 0, 0, 0); + static const QDate basedate(1858, 11, 17); + ISC_TIMESTAMP_TZ ts; + ts.utc_timestamp.timestamp_time = midnight.msecsTo(dt.time()) * 10; + ts.utc_timestamp.timestamp_date = basedate.daysTo(dt.date()); + ts.time_zone = qIanaIdToFbTzIdMap()->value(dt.timeZone().id().simplified(), 0); + return ts; +} +#endif + static ISC_TIME toTime(QTime t) { static const QTime midnight(0, 0, 0, 0); @@ -282,6 +337,34 @@ public: return true; } +#if (FB_API_VER >= 40) + void initTZMappingCache() + { + Q_Q(QIBaseDriver); + QSqlQuery qry(q->createResult()); + qry.setForwardOnly(true); + qry.exec(QString("select * from RDB$TIME_ZONES"_L1)); + if (qry.lastError().type()) { + q->setLastError(QSqlError( + QCoreApplication::translate("QIBaseDriver", + "failed to query time zone mapping from system table"), + qry.lastError().databaseText(), + QSqlError::StatementError, + qry.lastError().nativeErrorCode())); + + return; + } + + while (qry.next()) { + auto record = qry.record(); + quint16 fbTzId = record.value(0).value<quint16>(); + QByteArray ianaId = record.value(1).toByteArray().simplified(); + qFbTzIdToIanaIdMap()->insert(fbTzId, ianaId); + qIanaIdToFbTzIdMap()->insert(ianaId, fbTzId); + } + } +#endif + public: isc_db_handle ibase; isc_tr_handle trans; @@ -352,9 +435,9 @@ public: bool isSelect(); QVariant fetchBlob(ISC_QUAD *bId); - bool writeBlob(int i, const QByteArray &ba); + bool writeBlob(qsizetype iPos, const QByteArray &ba); QVariant fetchArray(int pos, ISC_QUAD *arr); - bool writeArray(int i, const QList<QVariant> &list); + bool writeArray(qsizetype i, const QList<QVariant> &list); public: ISC_STATUS status[20]; isc_tr_handle trans; @@ -374,8 +457,8 @@ QIBaseResultPrivate::QIBaseResultPrivate(QIBaseResult *q, const QIBaseDriver *dr localTransaction(!drv_d_func()->ibase), stmt(0), ibase(drv_d_func()->ibase), - sqlda(0), - inda(0), + sqlda(nullptr), + inda(nullptr), queryType(-1) { } @@ -399,20 +482,20 @@ void QIBaseResultPrivate::cleanup() q->cleanup(); } -bool QIBaseResultPrivate::writeBlob(int i, const QByteArray &ba) +bool QIBaseResultPrivate::writeBlob(qsizetype iPos, const QByteArray &ba) { isc_blob_handle handle = 0; - ISC_QUAD *bId = (ISC_QUAD*)inda->sqlvar[i].sqldata; + ISC_QUAD *bId = (ISC_QUAD*)inda->sqlvar[iPos].sqldata; isc_create_blob2(status, &ibase, &trans, &handle, bId, 0, 0); if (!isError(QT_TRANSLATE_NOOP("QIBaseResult", "Unable to create BLOB"), QSqlError::StatementError)) { - int i = 0; + qsizetype i = 0; while (i < ba.size()) { - isc_put_segment(status, &handle, qMin(ba.size() - i, int(QIBaseChunkSize)), - const_cast<char*>(ba.data()) + i); + isc_put_segment(status, &handle, qMin(ba.size() - i, QIBaseChunkSize), + ba.data() + i); if (isError(QT_TRANSLATE_NOOP("QIBaseResult", "Unable to write BLOB"))) return false; - i += qMin(ba.size() - i, int(QIBaseChunkSize)); + i += qMin(ba.size() - i, QIBaseChunkSize); } } isc_close_blob(status, &handle); @@ -432,7 +515,7 @@ QVariant QIBaseResultPrivate::fetchBlob(ISC_QUAD *bId) QByteArray ba; int chunkSize = QIBaseChunkSize; ba.resize(chunkSize); - int read = 0; + qsizetype read = 0; while (isc_get_segment(status, &handle, &len, chunkSize, ba.data() + read) == 0 || status[1] == isc_segment) { read += len; ba.resize(read + chunkSize); @@ -454,7 +537,7 @@ QVariant QIBaseResultPrivate::fetchBlob(ISC_QUAD *bId) } template<typename T> -static QList<QVariant> toList(char** buf, int count, T* = nullptr) +static QList<QVariant> toList(char** buf, int count) { QList<QVariant> res; for (int i = 0; i < count; ++i) { @@ -493,7 +576,7 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim, } break; } case blr_long: - valList = toList<int>(&buffer, numElements[dim], static_cast<int *>(0)); + valList = toList<int>(&buffer, numElements[dim]); break; case blr_short: valList = toList<short>(&buffer, numElements[dim]); @@ -513,8 +596,16 @@ static char* readArrayBuffer(QList<QVariant>& list, char *buffer, short curDim, buffer += sizeof(ISC_TIMESTAMP); } break; +#if (FB_API_VER >= 40) + case blr_timestamp_tz: + for (int i = 0; i < numElements[dim]; ++i) { + valList.append(fromTimeStampTz(buffer)); + buffer += sizeof(ISC_TIMESTAMP_TZ); + } + break; +#endif case blr_sql_time: - for(int i = 0; i < numElements[dim]; ++i) { + for (int i = 0; i < numElements[dim]; ++i) { valList.append(fromTime(buffer)); buffer += sizeof(ISC_TIME); } @@ -593,9 +684,8 @@ QVariant QIBaseResultPrivate::fetchArray(int pos, ISC_QUAD *arr) template<typename T> static char* fillList(char *buffer, const QList<QVariant> &list, T* = nullptr) { - for (int i = 0; i < list.size(); ++i) { - T val; - val = qvariant_cast<T>(list.at(i)); + for (const auto &elem : list) { + T val = qvariant_cast<T>(elem); memcpy(buffer, &val, sizeof(T)); buffer += sizeof(T); } @@ -605,11 +695,9 @@ static char* fillList(char *buffer, const QList<QVariant> &list, T* = nullptr) template<> char* fillList<float>(char *buffer, const QList<QVariant> &list, float*) { - for (int i = 0; i < list.size(); ++i) { - double val; - float val2 = 0; - val = qvariant_cast<double>(list.at(i)); - val2 = (float)val; + for (const auto &elem : list) { + double val = qvariant_cast<double>(elem); + float val2 = (float)val; memcpy(buffer, &val2, sizeof(float)); buffer += sizeof(float); } @@ -644,7 +732,6 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, QMetaType::Type type, short curDim, ISC_ARRAY_DESC *arrayDesc, QString& error) { - int i; ISC_ARRAY_BOUND *bounds = arrayDesc->array_desc_bounds; short dim = arrayDesc->array_desc_dimensions - 1; @@ -659,14 +746,14 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, } if (curDim != dim) { - for(i = 0; i < list.size(); ++i) { + for (const auto &elem : list) { - if (list.at(i).typeId() != QMetaType::QVariantList) { // dimensions mismatch + if (elem.typeId() != QMetaType::QVariantList) { // dimensions mismatch error = "Array dimensons mismatch. Fieldname: %1"_L1; return 0; } - buffer = createArrayBuffer(buffer, list.at(i).toList(), type, curDim + 1, + buffer = createArrayBuffer(buffer, elem.toList(), type, curDim + 1, arrayDesc, error); if (!buffer) return 0; @@ -693,29 +780,40 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, buffer = fillList<quint64>(buffer, list); break; case QMetaType::QString: - for (i = 0; i < list.size(); ++i) - buffer = qFillBufferWithString(buffer, list.at(i).toString(), + for (const auto &elem : list) + buffer = qFillBufferWithString(buffer, elem.toString(), arrayDesc->array_desc_length, arrayDesc->array_desc_dtype == blr_varying, true); break; case QMetaType::QDate: - for (i = 0; i < list.size(); ++i) { - *((ISC_DATE*)buffer) = toDate(list.at(i).toDate()); + for (const auto &elem : list) { + *((ISC_DATE*)buffer) = toDate(elem.toDate()); buffer += sizeof(ISC_DATE); } break; case QMetaType::QTime: - for (i = 0; i < list.size(); ++i) { - *((ISC_TIME*)buffer) = toTime(list.at(i).toTime()); + for (const auto &elem : list) { + *((ISC_TIME*)buffer) = toTime(elem.toTime()); buffer += sizeof(ISC_TIME); } break; - case QMetaType::QDateTime: - for (i = 0; i < list.size(); ++i) { - *((ISC_TIMESTAMP*)buffer) = toTimeStamp(list.at(i).toDateTime()); - buffer += sizeof(ISC_TIMESTAMP); + for (const auto &elem : list) { + switch (arrayDesc->array_desc_dtype) { + case blr_timestamp: + *((ISC_TIMESTAMP*)buffer) = toTimeStamp(elem.toDateTime()); + buffer += sizeof(ISC_TIMESTAMP); + break; +#if (FB_API_VER >= 40) + case blr_timestamp_tz: + *((ISC_TIMESTAMP_TZ*)buffer) = toTimeStampTz(elem.toDateTime()); + buffer += sizeof(ISC_TIMESTAMP_TZ); + break; +#endif + default: + break; + } } break; case QMetaType::Bool: @@ -728,7 +826,7 @@ static char* createArrayBuffer(char *buffer, const QList<QVariant> &list, return buffer; } -bool QIBaseResultPrivate::writeArray(int column, const QList<QVariant> &list) +bool QIBaseResultPrivate::writeArray(qsizetype column, const QList<QVariant> &list) { Q_Q(QIBaseResult); QString error; @@ -745,7 +843,6 @@ bool QIBaseResultPrivate::writeArray(int column, const QList<QVariant> &list) short arraySize = 1; ISC_LONG bufLen; - QList<QVariant> subList = list; short dimensions = desc.array_desc_dimensions; for(int i = 0; i < dimensions; ++i) { @@ -847,7 +944,7 @@ QIBaseResult::QIBaseResult(const QIBaseDriver *db) bool QIBaseResult::prepare(const QString& query) { Q_D(QIBaseResult); -// qDebug("prepare: %s", qPrintable(query)); +// qDebug("prepare: %ls", qUtf16Printable(query)); if (!driver() || !driver()->isOpen() || driver()->isOpenError()) return false; d->cleanup(); @@ -856,13 +953,13 @@ bool QIBaseResult::prepare(const QString& query) createDA(d->sqlda); if (d->sqlda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: createDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: createDA(): failed to allocate memory"; return false; } createDA(d->inda); if (d->inda == (XSQLDA*)0){ - qWarning()<<"QIOBaseResult: createDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: createDA(): failed to allocate memory"; return false; } @@ -886,7 +983,7 @@ bool QIBaseResult::prepare(const QString& query) if (d->inda->sqld > d->inda->sqln) { enlargeDA(d->inda, d->inda->sqld); if (d->inda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: enlargeDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: enlargeDA(): failed to allocate memory"; return false; } @@ -900,7 +997,7 @@ bool QIBaseResult::prepare(const QString& query) // need more field descriptors enlargeDA(d->sqlda, d->sqlda->sqld); if (d->sqlda == (XSQLDA*)0) { - qWarning()<<"QIOBaseResult: enlargeDA(): failed to allocate memory"; + qCWarning(lcIbase) << "QIOBaseResult: enlargeDA(): failed to allocate memory"; return false; } @@ -920,7 +1017,6 @@ bool QIBaseResult::prepare(const QString& query) return true; } - bool QIBaseResult::exec() { Q_D(QIBaseResult); @@ -936,20 +1032,17 @@ bool QIBaseResult::exec() if (d->inda) { const QList<QVariant> &values = boundValues(); - int i; if (values.count() > d->inda->sqld) { - qWarning() << "QIBaseResult::exec: Parameter mismatch, expected"_L1 << - d->inda->sqld << ", got"_L1 << values.count() << - "parameters"_L1; + qCWarning(lcIbase) << "QIBaseResult::exec: Parameter mismatch, expected"_L1 + << d->inda->sqld << ", got"_L1 << values.count() + << "parameters"_L1; return false; } - int para = 0; - for (i = 0; i < values.count(); ++i) { - para = i; + for (qsizetype para = 0; para < values.count(); ++para) { if (!d->inda->sqlvar[para].sqldata) // skip unknown datatypes continue; - const QVariant val(values[i]); + const QVariant &val = values[para]; if (d->inda->sqlvar[para].sqltype & 1) { if (QSqlResultPrivate::isVariantNull(val)) { // set null indicator @@ -961,6 +1054,12 @@ bool QIBaseResult::exec() } // a value of 0 means non-null. *(d->inda->sqlvar[para].sqlind) = 0; + } else { + if (QSqlResultPrivate::isVariantNull(val)) { + qCWarning(lcIbase) << "QIBaseResult::exec: Null value replaced by default (zero)"_L1 + << "value for type of column"_L1 << d->inda->sqlvar[para].ownname + << ", which is not nullable."_L1; + } } switch(d->inda->sqlvar[para].sqltype & ~1) { case SQL_INT64: @@ -997,6 +1096,11 @@ bool QIBaseResult::exec() case SQL_TIMESTAMP: *((ISC_TIMESTAMP*)d->inda->sqlvar[para].sqldata) = toTimeStamp(val.toDateTime()); break; +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: + *((ISC_TIMESTAMP_TZ*)d->inda->sqlvar[para].sqldata) = toTimeStampTz(val.toDateTime()); + break; +#endif case SQL_TYPE_TIME: *((ISC_TIME*)d->inda->sqlvar[para].sqldata) = toTime(val.toTime()); break; @@ -1019,8 +1123,8 @@ bool QIBaseResult::exec() *((bool*)d->inda->sqlvar[para].sqldata) = val.toBool(); break; default: - qWarning("QIBaseResult::exec: Unknown datatype %d", - d->inda->sqlvar[para].sqltype & ~1); + qCWarning(lcIbase, "QIBaseResult::exec: Unknown datatype %d", + d->inda->sqlvar[para].sqltype & ~1); break; } } @@ -1180,6 +1284,11 @@ bool QIBaseResult::gotoNext(QSqlCachedResult::ValueCache& row, int rowIdx) case SQL_BOOLEAN: row[idx] = QVariant(bool((*(bool*)buf))); break; +#if (FB_API_VER >= 40) + case SQL_TIMESTAMP_TZ: + row[idx] = fromTimeStampTz(buf); + break; +#endif default: // unknown type - don't even try to fetch row[idx] = QVariant(); @@ -1286,7 +1395,7 @@ int QIBaseResult::numRowsAffected() bIsProcedure = true; // will sum all changes break; default: - qWarning() << "numRowsAffected: Unknown statement type (" << d->queryType << ")"; + qCWarning(lcIbase) << "numRowsAffected: Unknown statement type (" << d->queryType << ")"; return -1; } @@ -1352,7 +1461,6 @@ QSqlRecord QIBaseResult::record() const f.setRequiredStatus(q.value(3).toBool() ? QSqlField::Required : QSqlField::Optional); } } - f.setSqlType(v.sqltype); rec.append(f); } return rec; @@ -1408,26 +1516,26 @@ bool QIBaseDriver::hasFeature(DriverFeature f) const return false; } -bool QIBaseDriver::open(const QString & db, - const QString & user, - const QString & password, - const QString & host, - int port, - const QString & connOpts) +bool QIBaseDriver::open(const QString &db, + const QString &user, + const QString &password, + const QString &host, + int port, + const QString &connOpts) { Q_D(QIBaseDriver); if (isOpen()) close(); - const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts)); + const auto opts(QStringView(connOpts).split(u';', Qt::SkipEmptyParts)); QByteArray role; - for (int i = 0; i < opts.count(); ++i) { - QString tmp(opts.at(i).simplified()); + for (const auto &opt : opts) { + const auto tmp(opt.trimmed()); qsizetype idx; if ((idx = tmp.indexOf(u'=')) != -1) { - QString val = tmp.mid(idx + 1).simplified(); - QString opt = tmp.left(idx).simplified(); + const auto val = tmp.mid(idx + 1).trimmed(); + const auto opt = tmp.left(idx).trimmed().toString(); if (opt.toUpper() == "ISC_DPB_SQL_ROLE_NAME"_L1) { role = val.toLocal8Bit(); role.truncate(255); @@ -1478,6 +1586,14 @@ bool QIBaseDriver::open(const QString & db, setOpen(true); setOpenError(false); +#if (FB_API_VER >= 40) + std::call_once(initTZMappingFlag, [d](){ d->initTZMappingCache(); }); + if (lastError().isValid()) + { + setOpen(true); + return false; + } +#endif return true; } @@ -1496,13 +1612,6 @@ void QIBaseDriver::close() qFreeEventBuffer(eBuffer); } d->eventBuffers.clear(); - -#if defined(FB_API_VER) - // TODO check whether this workaround for Firebird crash is still needed - QDeadlineTimer timer(500); - while (!timer.hasExpired()) - QCoreApplication::processEvents(); -#endif } isc_detach_database(d->status, &d->ibase); @@ -1587,8 +1696,8 @@ QStringList QIBaseDriver::tables(QSql::TableType type) const q.setForwardOnly(true); if (!q.exec("select rdb$relation_name from rdb$relations "_L1 + typeFilter)) return res; - while(q.next()) - res << q.value(0).toString().simplified(); + while (q.next()) + res << q.value(0).toString().simplified(); return res; } @@ -1599,13 +1708,9 @@ QSqlRecord QIBaseDriver::record(const QString& tablename) const if (!isOpen()) return rec; + const QString table = stripDelimiters(tablename, QSqlDriver::TableName); QSqlQuery q(createResult()); q.setForwardOnly(true); - QString table = tablename; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = table.toUpper(); q.exec("SELECT a.RDB$FIELD_NAME, b.RDB$FIELD_TYPE, b.RDB$FIELD_LENGTH, " "b.RDB$FIELD_SCALE, b.RDB$FIELD_PRECISION, a.RDB$NULL_FLAG " "FROM RDB$RELATION_FIELDS a, RDB$FIELDS b " @@ -1625,7 +1730,6 @@ QSqlRecord QIBaseDriver::record(const QString& tablename) const f.setPrecision(0); } f.setRequired(q.value(5).toInt() > 0); - f.setSqlType(type); rec.append(f); } @@ -1638,12 +1742,7 @@ QSqlIndex QIBaseDriver::primaryIndex(const QString &table) const if (!isOpen()) return index; - QString tablename = table; - if (isIdentifierEscaped(tablename, QSqlDriver::TableName)) - tablename = stripDelimiters(tablename, QSqlDriver::TableName); - else - tablename = tablename.toUpper(); - + const QString tablename = stripDelimiters(table, QSqlDriver::TableName); QSqlQuery q(createResult()); q.setForwardOnly(true); q.exec("SELECT a.RDB$INDEX_NAME, b.RDB$FIELD_NAME, d.RDB$FIELD_TYPE, d.RDB$FIELD_SCALE " @@ -1738,13 +1837,13 @@ bool QIBaseDriver::subscribeToNotification(const QString &name) { Q_D(QIBaseDriver); if (!isOpen()) { - qWarning("QIBaseDriver::subscribeFromNotificationImplementation: database not open."); + qCWarning(lcIbase, "QIBaseDriver::subscribeFromNotificationImplementation: database not open."); return false; } if (d->eventBuffers.contains(name)) { - qWarning("QIBaseDriver::subscribeToNotificationImplementation: already subscribing to '%s'.", - qPrintable(name)); + qCWarning(lcIbase, "QIBaseDriver::subscribeToNotificationImplementation: already subscribing to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1785,13 +1884,13 @@ bool QIBaseDriver::unsubscribeFromNotification(const QString &name) { Q_D(QIBaseDriver); if (!isOpen()) { - qWarning("QIBaseDriver::unsubscribeFromNotificationImplementation: database not open."); + qCWarning(lcIbase, "QIBaseDriver::unsubscribeFromNotificationImplementation: database not open."); return false; } if (!d->eventBuffers.contains(name)) { - qWarning("QIBaseDriver::QIBaseSubscriptionState not subscribed to '%s'.", - qPrintable(name)); + qCWarning(lcIbase, "QIBaseDriver::QIBaseSubscriptionState not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1846,8 +1945,8 @@ void QIBaseDriver::qHandleEventNotification(void *updatedResultBuffer) (&qEventCallback)), eBuffer->resultBuffer); if (Q_UNLIKELY(status[0] == 1 && status[1])) { - qCritical("QIBaseDriver::qHandleEventNotification: could not resubscribe to '%s'", - qPrintable(i.key())); + qCritical("QIBaseDriver::qHandleEventNotification: could not resubscribe to '%ls'", + qUtf16Printable(i.key())); } return; @@ -1860,8 +1959,8 @@ QString QIBaseDriver::escapeIdentifier(const QString &identifier, IdentifierType QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } @@ -1873,3 +1972,5 @@ int QIBaseDriver::maximumIdentifierLength(IdentifierType type) const } QT_END_NAMESPACE + +#include "moc_qsql_ibase_p.cpp" diff --git a/src/plugins/sqldrivers/mimer/CMakeLists.txt b/src/plugins/sqldrivers/mimer/CMakeLists.txt new file mode 100644 index 0000000000..303af7120c --- /dev/null +++ b/src/plugins/sqldrivers/mimer/CMakeLists.txt @@ -0,0 +1,24 @@ +# Generated from mimer.pro. + +##################################################################### +## MIMERSQLDriverPlugin Plugin: +##################################################################### + +qt_internal_add_plugin(QMimerSQLDriverPlugin + OUTPUT_NAME qsqlmimer + PLUGIN_TYPE sqldrivers + SOURCES + main.cpp + qsql_mimer.cpp qsql_mimer.h + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT + LIBRARIES + MimerSQL::MimerSQL + Qt::Core + Qt::SqlPrivate +) + +#### Keys ignored in scope 1:.:.:mimer.pro:<TRUE>: +# OTHER_FILES = "mimer.json" diff --git a/src/plugins/sqldrivers/mimer/README b/src/plugins/sqldrivers/mimer/README new file mode 100644 index 0000000000..02e4c3e162 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/README @@ -0,0 +1,6 @@ +You will need the Mimer SQL development headers and libraries installed before +compiling this plugin. qsql_mimer.h contains an include to mimerapi.h that is +needed for the driver to compile. + +See the Qt SQL documentation for more information on compiling Qt SQL driver +plugins. diff --git a/src/plugins/sqldrivers/mimer/main.cpp b/src/plugins/sqldrivers/mimer/main.cpp new file mode 100644 index 0000000000..560b7da7c7 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/main.cpp @@ -0,0 +1,33 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include "qsql_mimer.h" + +#include <qsqldriverplugin.h> +#include <qstringlist.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +class QMimerSQLDriverPlugin : public QSqlDriverPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QSqlDriverFactoryInterface" FILE "mimer.json") +public: + QMimerSQLDriverPlugin(); + QSqlDriver *create(const QString &) override; +}; + +QMimerSQLDriverPlugin::QMimerSQLDriverPlugin() : QSqlDriverPlugin() { } + +QSqlDriver *QMimerSQLDriverPlugin::create(const QString &name) +{ + if (name == "QMIMER"_L1) + return new QMimerSQLDriver; + return nullptr; +} + +QT_END_NAMESPACE + +#include "main.moc" diff --git a/src/plugins/sqldrivers/mimer/mimer.json b/src/plugins/sqldrivers/mimer/mimer.json new file mode 100644 index 0000000000..fba96b765d --- /dev/null +++ b/src/plugins/sqldrivers/mimer/mimer.json @@ -0,0 +1,5 @@ +{ + "Keys": [ + "QMIMER" + ] +} diff --git a/src/plugins/sqldrivers/mimer/qsql_mimer.cpp b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp new file mode 100644 index 0000000000..7f89e0a0d5 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/qsql_mimer.cpp @@ -0,0 +1,1610 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#include <qcoreapplication.h> +#include <qvariant.h> +#include <qmetatype.h> +#include <qdatetime.h> +#include <qloggingcategory.h> +#include <qsqlerror.h> +#include <qsqlfield.h> +#include <qsqlindex.h> +#include <qsqlrecord.h> +#include <qsqlquery.h> +#include <qsocketnotifier.h> +#include <qstringlist.h> +#include <qlocale.h> +#if defined(Q_OS_WIN32) +# include <QtCore/qt_windows.h> +#endif +#include <QtSql/private/qsqlresult_p.h> +#include <QtSql/private/qsqldriver_p.h> +#include "qsql_mimer.h" + +#define MIMER_DEFAULT_DATATYPE 1000 + +Q_DECLARE_OPAQUE_POINTER(MimerSession) +Q_DECLARE_METATYPE(MimerSession) + +Q_DECLARE_OPAQUE_POINTER(MimerStatement) +Q_DECLARE_METATYPE(MimerStatement) + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(lcMimer, "qt.sql.mimer") + +enum class MimerColumnTypes { + Binary, + Clob, + Blob, + String, + Int, + Numeric, + Long, + Float, + Double, + Boolean, + Uuid, + Date, + Time, + Timestamp, + Unknown +}; + +using namespace Qt::StringLiterals; + +class QMimerSQLResultPrivate; + +class QMimerSQLResult final : public QSqlResult +{ + Q_DECLARE_PRIVATE(QMimerSQLResult) +public: + QMimerSQLResult(const QMimerSQLDriver *db); + virtual ~QMimerSQLResult() override; + QVariant handle() const override; + static constexpr int genericError = -1; + static constexpr int lobChunkMaxSizeSet = 1048500; + static constexpr int lobChunkMaxSizeFetch = 65536; + static constexpr int maxStackStringSize = 200; + static constexpr int maxTimeStringSize = 18; + static constexpr int maxDateStringSize = 10; + static constexpr int maxTimestampStringSize = 29; + +private: + void cleanup(); + bool fetch(int i) override; + bool fetchFirst() override; + bool fetchLast() override; + bool fetchNext() override; + QVariant data(int i) override; + bool isNull(int index) override; + bool reset(const QString &query) override; + int size() override; + int numRowsAffected() override; + QSqlRecord record() const override; + bool prepare(const QString &query) override; + bool execBatch(bool arrayBind = false) override; + bool exec() override; + qint64 currentRow(); + QVariant lastInsertId() const override; +}; + +class QMimerSQLDriverPrivate final : public QSqlDriverPrivate +{ + Q_DECLARE_PUBLIC(QMimerSQLDriver) +public: + QMimerSQLDriverPrivate() : QSqlDriverPrivate(QSqlDriver::MimerSQL), sessionhandle(nullptr) { } + MimerSession sessionhandle; + QString dbName; + QString dbUser; + void splitTableQualifier(const QString &qualifier, QString *schema, QString *table) const; +}; + +class QMimerSQLResultPrivate : public QSqlResultPrivate +{ + Q_DECLARE_PUBLIC(QMimerSQLResult) +public: + Q_DECLARE_SQLDRIVER_PRIVATE(QMimerSQLDriver) + QMimerSQLResultPrivate(QMimerSQLResult *q, const QMimerSQLDriver *drv) + : QSqlResultPrivate(q, drv), + statementhandle(nullptr), + lobhandle(nullptr), + rowsAffected(0), + preparedQuery(false), + openCursor(false), + openStatement(false), + executedStatement(false), + callWithOut(false), + execBatch(false), + currentRow(QSql::BeforeFirstRow) + { + } + MimerStatement statementhandle; + MimerLob lobhandle; + int rowsAffected; + bool preparedQuery; + bool openCursor; + bool openStatement; + bool executedStatement; + bool callWithOut; + bool execBatch; + qint64 currentSize = -1; + qint64 currentRow; // Only used when forwardOnly() + QVector<QVariant> batch_vector; +}; + +static QSqlError qMakeError(const QString &err, const int errCode, QSqlError::ErrorType type, + const QMimerSQLDriverPrivate *p) +{ + QString msg; + if (p) { + size_t str_len; + int e_code; + int rc; + str_len = (rc = MimerGetError(p->sessionhandle, &e_code, NULL, 0)) + 1; + if (!MIMER_SUCCEEDED(rc)) { + msg = QCoreApplication::translate("QMimerSQL", "No Mimer SQL error for code %1") + .arg(errCode); + } else { + QVarLengthArray<wchar_t> tmp_buff((qsizetype)str_len); + if (!MIMER_SUCCEEDED( + rc = MimerGetError(p->sessionhandle, &e_code, tmp_buff.data(), str_len))) + msg = QCoreApplication::translate("QMimerSQL", "No Mimer SQL error for code %1") + .arg(errCode); + else + msg = QString::fromWCharArray(tmp_buff.data()); + } + } else { + msg = QCoreApplication::translate("QMimerSQL", "Generic Mimer SQL error"); + } + + return QSqlError("QMIMER: "_L1 + err, msg, type, QString::number(errCode)); +} + +static QString msgCouldNotGet(const char *type, int column) +{ + //: Data type, column + return QCoreApplication::translate("QMimerSQLResult", + "Could not get %1, column %2").arg(QLatin1StringView(type)).arg(column); +} + +static QString msgCouldNotSet(const char *type, int column) +{ + //: Data type, parameter + return QCoreApplication::translate("QMimerSQLResult", + "Could not set %1, parameter %2").arg(QLatin1StringView(type)).arg(column); +} + +QMimerSQLDriver::QMimerSQLDriver(QObject *parent) : QSqlDriver(*new QMimerSQLDriverPrivate, parent) +{ +} + +QMimerSQLDriver::QMimerSQLDriver(MimerSession *conn, QObject *parent) + : QSqlDriver(*new QMimerSQLDriverPrivate, parent) +{ + Q_D(QMimerSQLDriver); + if (conn) + d->sessionhandle = *conn; +} + +QMimerSQLDriver::~QMimerSQLDriver() +{ + close(); +} + +QMimerSQLResult::QMimerSQLResult(const QMimerSQLDriver *db) + : QSqlResult(*new QMimerSQLResultPrivate(this, db)) +{ + Q_D(QMimerSQLResult); + d->preparedQuery = db->hasFeature(QSqlDriver::PreparedQueries); +} + +QMimerSQLResult::~QMimerSQLResult() +{ + cleanup(); +} + +static MimerColumnTypes mimerMapColumnTypes(int32_t t) +{ + switch (t) { + case MIMER_BINARY: + case MIMER_BINARY_VARYING: + return MimerColumnTypes::Binary; + case MIMER_BLOB: + case MIMER_NATIVE_BLOB: + return MimerColumnTypes::Blob; + case MIMER_CLOB: + case MIMER_NCLOB: + case MIMER_NATIVE_CLOB: + case MIMER_NATIVE_NCLOB: + return MimerColumnTypes::Clob; + case MIMER_DATE: + return MimerColumnTypes::Date; + case MIMER_TIME: + return MimerColumnTypes::Time; + case MIMER_TIMESTAMP: + return MimerColumnTypes::Timestamp; + case MIMER_INTERVAL_DAY: + case MIMER_INTERVAL_DAY_TO_HOUR: + case MIMER_INTERVAL_DAY_TO_MINUTE: + case MIMER_INTERVAL_DAY_TO_SECOND: + case MIMER_INTERVAL_HOUR: + case MIMER_INTERVAL_HOUR_TO_MINUTE: + case MIMER_INTERVAL_HOUR_TO_SECOND: + case MIMER_INTERVAL_MINUTE: + case MIMER_INTERVAL_MINUTE_TO_SECOND: + case MIMER_INTERVAL_MONTH: + case MIMER_INTERVAL_SECOND: + case MIMER_INTERVAL_YEAR: + case MIMER_INTERVAL_YEAR_TO_MONTH: + case MIMER_NCHAR: + case MIMER_CHARACTER: + case MIMER_CHARACTER_VARYING: + case MIMER_NCHAR_VARYING: + case MIMER_UTF8: + case MIMER_DEFAULT_DATATYPE: + return MimerColumnTypes::String; + case MIMER_INTEGER: + case MIMER_DECIMAL: + case MIMER_FLOAT: + return MimerColumnTypes::Numeric; + case MIMER_BOOLEAN: + return MimerColumnTypes::Boolean; + case MIMER_T_BIGINT: + case MIMER_T_UNSIGNED_BIGINT: + case MIMER_NATIVE_BIGINT_NULLABLE: + case MIMER_NATIVE_BIGINT: + return MimerColumnTypes::Long; + case MIMER_NATIVE_REAL_NULLABLE: + case MIMER_NATIVE_REAL: + case MIMER_T_REAL: + return MimerColumnTypes::Float; + case MIMER_T_FLOAT: + case MIMER_NATIVE_DOUBLE_NULLABLE: + case MIMER_NATIVE_DOUBLE: + case MIMER_T_DOUBLE: + return MimerColumnTypes::Double; + case MIMER_NATIVE_INTEGER: + case MIMER_NATIVE_INTEGER_NULLABLE: + case MIMER_NATIVE_SMALLINT_NULLABLE: + case MIMER_NATIVE_SMALLINT: + case MIMER_T_INTEGER: + case MIMER_T_SMALLINT: + return MimerColumnTypes::Int; + case MIMER_UUID: + return MimerColumnTypes::Uuid; + default: + qCWarning(lcMimer) << "QMimerSQLDriver::mimerMapColumnTypes: Unknown data type:" << t; + } + return MimerColumnTypes::Unknown; +} + +static QMetaType::Type qDecodeMSQLType(int32_t t) +{ + switch (t) { + case MIMER_BINARY: + case MIMER_BINARY_VARYING: + case MIMER_BLOB: + case MIMER_NATIVE_BLOB: + return QMetaType::QByteArray; + case MIMER_CLOB: + case MIMER_NCLOB: + case MIMER_NATIVE_CLOB: + case MIMER_NATIVE_NCLOB: + case MIMER_INTERVAL_DAY: + case MIMER_DECIMAL: + case MIMER_INTERVAL_DAY_TO_HOUR: + case MIMER_INTERVAL_DAY_TO_MINUTE: + case MIMER_INTERVAL_DAY_TO_SECOND: + case MIMER_INTERVAL_HOUR: + case MIMER_INTERVAL_HOUR_TO_MINUTE: + case MIMER_INTERVAL_HOUR_TO_SECOND: + case MIMER_INTERVAL_MINUTE: + case MIMER_INTERVAL_MINUTE_TO_SECOND: + case MIMER_INTERVAL_MONTH: + case MIMER_INTERVAL_SECOND: + case MIMER_INTERVAL_YEAR: + case MIMER_INTERVAL_YEAR_TO_MONTH: + case MIMER_NCHAR: + case MIMER_CHARACTER: + case MIMER_CHARACTER_VARYING: + case MIMER_NCHAR_VARYING: + case MIMER_UTF8: + case MIMER_DEFAULT_DATATYPE: + case MIMER_INTEGER: + case MIMER_FLOAT: + return QMetaType::QString; + case MIMER_BOOLEAN: + return QMetaType::Bool; + case MIMER_T_BIGINT: + case MIMER_T_UNSIGNED_BIGINT: + case MIMER_NATIVE_BIGINT_NULLABLE: + case MIMER_NATIVE_BIGINT: + return QMetaType::LongLong; + case MIMER_NATIVE_REAL_NULLABLE: + case MIMER_NATIVE_REAL: + case MIMER_T_REAL: + return QMetaType::Float; + case MIMER_T_FLOAT: + case MIMER_NATIVE_DOUBLE_NULLABLE: + case MIMER_NATIVE_DOUBLE: + case MIMER_T_DOUBLE: + return QMetaType::Double; + case MIMER_NATIVE_INTEGER_NULLABLE: + case MIMER_T_INTEGER: + case MIMER_NATIVE_INTEGER: + return QMetaType::Int; + case MIMER_NATIVE_SMALLINT_NULLABLE: + case MIMER_T_SMALLINT: + return QMetaType::Int; + case MIMER_DATE: + return QMetaType::QDate; + case MIMER_TIME: + return QMetaType::QTime; + break; + case MIMER_TIMESTAMP: + return QMetaType::QDateTime; + case MIMER_UUID: + return QMetaType::QUuid; + default: + qCWarning(lcMimer) << "QMimerSQLDriver::qDecodeMSQLType: Unknown data type:" << t; + return QMetaType::UnknownType; + } +} + +static int32_t qLookupMimDataType(QStringView s) +{ + if (s == u"BINARY") + return MIMER_BINARY; + if (s == u"BINARY VARYING") + return MIMER_BINARY_VARYING; + if (s == u"BINARY LARGE OBJECT") + return MIMER_BLOB; + if (s == u"CHARACTER LARGE OBJECT") + return MIMER_CLOB; + if (s == u"NATIONAL CHAR LARGE OBJECT") + return MIMER_NCLOB; + if (s == u"INTERVAL DAY") + return MIMER_INTERVAL_DAY; + if (s == u"DECIMAL") + return MIMER_DECIMAL; + if (s == u"INTERVAL DAY TO HOUR") + return MIMER_INTERVAL_DAY_TO_HOUR; + if (s == u"INTERVAL DAY TO MINUTE") + return MIMER_INTERVAL_DAY_TO_MINUTE; + if (s == u"INTERVAL DAY TO SECOND") + return MIMER_INTERVAL_DAY_TO_SECOND; + if (s == u"INTERVAL HOUR") + return MIMER_INTERVAL_HOUR; + if (s == u"INTERVAL HOUR TO MINUTE") + return MIMER_INTERVAL_HOUR_TO_MINUTE; + if (s == u"INTERVAL HOUR TO SECOND") + return MIMER_INTERVAL_HOUR_TO_SECOND; + if (s == u"INTERVAL MINUTE") + return MIMER_INTERVAL_MINUTE; + if (s == u"INTERVAL MINUTE TO SECOND") + return MIMER_INTERVAL_MINUTE_TO_SECOND; + if (s == u"INTERVAL MONTH") + return MIMER_INTERVAL_MONTH; + if (s == u"INTERVAL SECOND") + return MIMER_INTERVAL_SECOND; + if (s == u"INTERVAL YEAR") + return MIMER_INTERVAL_YEAR; + if (s == u"INTERVAL YEAR TO MONTH") + return MIMER_INTERVAL_YEAR_TO_MONTH; + if (s == u"NATIONAL CHARACTER") + return MIMER_NCHAR; + if (s == u"CHARACTER") + return MIMER_CHARACTER; + if (s == u"CHARACTER VARYING") + return MIMER_CHARACTER_VARYING; + if (s == u"NATIONAL CHARACTER VARYING") + return MIMER_NCHAR_VARYING; + if (s == u"UTF-8") + return MIMER_UTF8; + if (s == u"BOOLEAN") + return MIMER_BOOLEAN; + if (s == u"BIGINT") + return MIMER_T_BIGINT; + if (s == u"REAL") + return MIMER_T_REAL; + if (s == u"FLOAT") + return MIMER_T_FLOAT; + if (s == u"DOUBLE PRECISION") + return MIMER_T_DOUBLE; + if (s == u"INTEGER") + return MIMER_T_INTEGER; + if (s == u"SMALLINT") + return MIMER_T_SMALLINT; + if (s == u"DATE") + return MIMER_DATE; + if (s == u"TIME") + return MIMER_TIME; + if (s == u"TIMESTAMP") + return MIMER_TIMESTAMP; + if (s == u"BUILTIN.UUID") + return MIMER_UUID; + if (s == u"USER-DEFINED") + return MIMER_DEFAULT_DATATYPE; + qCWarning(lcMimer) << "QMimerSQLDriver::qLookupMimDataType: Unhandled data type:" << s; + return MIMER_DEFAULT_DATATYPE; +} + +QVariant QMimerSQLResult::handle() const +{ + Q_D(const QMimerSQLResult); + return QVariant::fromValue(d->statementhandle); +} + +void QMimerSQLResult::cleanup() +{ + Q_D(QMimerSQLResult); + if (!driver() || !driver()->isOpen()) { + d->openCursor = false; + d->openStatement = false; + return; + } + if (d->openCursor) { + const int32_t err = MimerCloseCursor(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not close cursor"), err, + QSqlError::StatementError, d->drv_d_func())); + d->openCursor = false; + } + if (d->openStatement) { + const int32_t err = MimerEndStatement(&d->statementhandle); + if (!MIMER_SUCCEEDED(err)) + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not close statement"), + err, QSqlError::StatementError, d->drv_d_func())); + d->openStatement = false; + } + d->currentSize = -1; +} + +qint64 QMimerSQLResult::currentRow() +{ + Q_D(const QMimerSQLResult); + return d->currentRow; +} + +bool QMimerSQLResult::fetch(int i) +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (i == at()) + return true; + if (i < 0) + return false; + + if (isForwardOnly() && i < at()) + return false; + + if (isForwardOnly()) { + bool rc; + do { + rc = fetchNext(); + } while (rc && currentRow() < i); + return rc; + } else { + err = MimerFetchScroll(d->statementhandle, MIMER_ABSOLUTE, i + 1); + if (err == MIMER_NO_DATA) + return false; + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(QCoreApplication::translate("QMimerSQLResult", "Fetch did not succeed"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + setAt(MimerCurrentRow(d->statementhandle) - 1); + return true; +} + +bool QMimerSQLResult::fetchFirst() +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) { + if (currentRow() < 0) + return fetchNext(); + else if (currentRow() == 0) + setAt(0); + else + return false; + } else { + err = MimerFetchScroll(d->statementhandle, MIMER_FIRST, 0); + if (MIMER_SUCCEEDED(err) && err != MIMER_NO_DATA) + setAt(0); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Fetch first did not succeed"), err, + QSqlError::StatementError, d->drv_d_func())); + return false; + } + if (err == MIMER_NO_DATA) + return false; + return true; +} + +bool QMimerSQLResult::fetchLast() +{ + Q_D(const QMimerSQLResult); + int32_t err = 0; + int row = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) { + bool rc; + do { + rc = fetchNext(); + } while (rc); + + return currentRow() >= 0; + } else { + err = MimerFetchScroll(d->statementhandle, static_cast<std::int32_t>(MIMER_LAST), 0); + if (err == MIMER_NO_DATA) + return false; + if (MIMER_SUCCEEDED(err)) { + row = MimerCurrentRow(d->statementhandle) - 1; + } else { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Fetch last did not succeed"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } + + if (row < 0) { + setAt(QSql::BeforeFirstRow); + return false; + } else { + setAt(row); + return true; + } +} + +bool QMimerSQLResult::fetchNext() +{ + Q_D(QMimerSQLResult); + int32_t err = 0; + if (!isActive() || !isSelect()) + return false; + if (isForwardOnly()) + err = MimerFetch(d->statementhandle); + else + err = MimerFetchScroll(d->statementhandle, MIMER_NEXT, 0); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not fetch next row"), err, + QSqlError::StatementError, d->drv_d_func())); + if (isForwardOnly()) + d->currentRow = QSql::BeforeFirstRow; + return false; + } + if (err == MIMER_NO_DATA) + return false; + if (isForwardOnly()) + setAt(++d->currentRow); + else + setAt(MimerCurrentRow(d->statementhandle) - 1); + return true; +} + +QVariant QMimerSQLResult::data(int i) +{ + Q_D(QMimerSQLResult); + int32_t err; + int32_t mType; + if (d->callWithOut) { + if (i >= MimerParameterCount(d->statementhandle)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Column %1 out of range") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return QVariant(); + } + mType = MimerParameterType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + } else { + if (i >= MimerColumnCount(d->statementhandle)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult:", "Column %1 out of range") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return QVariant(); + } + mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + } + const QMetaType::Type type = qDecodeMSQLType(mType); + const MimerColumnTypes mimDataType = mimerMapColumnTypes(mType); + err = MimerIsNull(d->statementhandle, static_cast<std::int16_t>(i + 1)); + if (err > 0) { + return QVariant(QMetaType(type), nullptr); + } else { + switch (mimDataType) { + case MimerColumnTypes::Date: { + wchar_t dateString_w[maxDateStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), dateString_w, + sizeof(dateString_w) / sizeof(dateString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("date", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return QDate::fromString(QString::fromWCharArray(dateString_w), "yyyy-MM-dd"_L1); + } + case MimerColumnTypes::Time: { + wchar_t timeString_w[maxTimeStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), timeString_w, + sizeof(timeString_w) / sizeof(timeString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + QString timeString = QString::fromWCharArray(timeString_w); + QString timeFormatString = "HH:mm:ss"_L1; + if (timeString.size() > 8) { + timeFormatString.append(".zzz"_L1); + timeString = timeString.left(12); + } + return QTime::fromString(timeString, timeFormatString); + } + case MimerColumnTypes::Timestamp: { + wchar_t dateTimeString_w[maxTimestampStringSize + 1]; + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + dateTimeString_w, + sizeof(dateTimeString_w) / sizeof(dateTimeString_w[0])); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("date time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + QString dateTimeString = QString::fromWCharArray(dateTimeString_w); + QString dateTimeFormatString = "yyyy-MM-dd HH:mm:ss"_L1; + if (dateTimeString.size() > 19) { + dateTimeFormatString.append(".zzz"_L1); + dateTimeString = dateTimeString.left(23); + } + return QDateTime::fromString(dateTimeString, dateTimeFormatString); + } + case MimerColumnTypes::Int: { + int resInt; + err = MimerGetInt32(d->statementhandle, static_cast<std::int16_t>(i + 1), &resInt); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("int32", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return resInt; + } + case MimerColumnTypes::Long: { + int64_t resLongLong; + err = MimerGetInt64(d->statementhandle, static_cast<std::int16_t>(i + 1), &resLongLong); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("int64", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return (qlonglong)resLongLong; + } + case MimerColumnTypes::Boolean: { + err = MimerGetBoolean(d->statementhandle, static_cast<std::int16_t>(i + 1)); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("boolean", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return err == 1; + } + case MimerColumnTypes::Float: { + float resFloat; + err = MimerGetFloat(d->statementhandle, static_cast<std::int16_t>(i + 1), &resFloat); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("float", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return resFloat; + } + case MimerColumnTypes::Double: { + double resDouble; + err = MimerGetDouble(d->statementhandle, static_cast<std::int16_t>(i + 1), &resDouble); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("double", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + switch (numericalPrecisionPolicy()) { + case QSql::LowPrecisionInt32: + return static_cast<std::int32_t>(resDouble); + case QSql::LowPrecisionInt64: + return static_cast<qint64>(resDouble); + case QSql::LowPrecisionDouble: + return static_cast<qreal>(resDouble); + case QSql::HighPrecision: + return QString::number(resDouble, 'g', 17); + } + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Binary: { + QByteArray byteArray; + // Get size + err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i + 1), NULL, 0); + if (MIMER_SUCCEEDED(err)) { + byteArray.resize(err); + err = MimerGetBinary(d->statementhandle, static_cast<std::int16_t>(i + 1), + byteArray.data(), err); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotGet("binary", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return byteArray; + } + case MimerColumnTypes::Blob: { + QByteArray byteArray; + size_t size; + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, + &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeFetch; + QVarLengthArray<char> blobchar(lobChunkMaxSizeFetch); + byteArray.reserve(size); + size_t left_to_return = size; + while (left_to_return > 0) { + const size_t bytesToReceive = + left_to_return <= maxSize ? left_to_return : maxSize; + err = MimerGetBlobData(&d->lobhandle, blobchar.data(), bytesToReceive); + byteArray.append(QByteArray::fromRawData(blobchar.data(), bytesToReceive)); + left_to_return -= bytesToReceive; + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("BLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + } + } else { + setLastError(qMakeError(msgCouldNotGet("BLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + return byteArray; + } + case MimerColumnTypes::Numeric: + case MimerColumnTypes::String: { + wchar_t resString_w[maxStackStringSize + 1]; + // Get size + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), resString_w, + 0); + if (MIMER_SUCCEEDED(err)) { + int size = err; + if (err <= maxStackStringSize) { // For smaller strings, use a small buffer for + // efficiency + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + resString_w, maxStackStringSize + 1); + if (MIMER_SUCCEEDED(err)) + return QString::fromWCharArray(resString_w); + } else { // For larger strings, dynamically allocate memory + QVarLengthArray<wchar_t> largeResString_w(size + 1); + err = MimerGetString(d->statementhandle, static_cast<std::int16_t>(i + 1), + largeResString_w.data(), size + 1); + if (MIMER_SUCCEEDED(err)) + return QString::fromWCharArray(largeResString_w.data()); + } + } + setLastError(qMakeError(msgCouldNotGet( + mimDataType == MimerColumnTypes::Numeric ? "numeric" : "string", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Clob: { + size_t size; + err = MimerGetLob(d->statementhandle, static_cast<std::int16_t>(i + 1), &size, + &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeFetch; + QVarLengthArray<wchar_t> clobstring_w(lobChunkMaxSizeFetch + 1); + + size_t left_to_return = size; + QString returnString; + while (left_to_return > 0) { + const size_t bytesToReceive = + left_to_return <= maxSize ? left_to_return : maxSize; + err = MimerGetNclobData(&d->lobhandle, clobstring_w.data(), bytesToReceive + 1); + returnString.append(QString::fromWCharArray(clobstring_w.data())); + left_to_return -= bytesToReceive; + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + } + return returnString; + } + setLastError(qMakeError(msgCouldNotGet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + case MimerColumnTypes::Uuid: { + unsigned char uuidChar[16]; + err = MimerGetUUID(d->statementhandle, static_cast<std::int16_t>(i + 1), uuidChar); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotGet("UUID", i), + err, QSqlError::StatementError, d->drv_d_func())); + return QVariant(QMetaType(type), nullptr); + } + const QByteArray uuidByteArray = QByteArray(reinterpret_cast<char *>(uuidChar), 16); + return QUuid::fromRfc4122(uuidByteArray); + } + case MimerColumnTypes::Unknown: + default: + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Unknown data type %1").arg(i), + genericError, QSqlError::StatementError, nullptr)); + } + return QVariant(QMetaType(type), nullptr); + } +} + +bool QMimerSQLResult::isNull(int index) +{ + Q_D(const QMimerSQLResult); + const int32_t rc = MimerIsNull(d->statementhandle, static_cast<std::int16_t>(index + 1)); + if (!MIMER_SUCCEEDED(rc)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not check null, column %1") + .arg(index), + rc, QSqlError::StatementError, d->drv_d_func())); + return false; + } + return rc != 0; +} + +bool QMimerSQLResult::reset(const QString &query) +{ + if (!prepare(query)) + return false; + return exec(); +} + +int QMimerSQLResult::size() +{ + Q_D(QMimerSQLResult); + if (!isActive() || !isSelect() || isForwardOnly()) + return -1; + + if (d->currentSize != -1) + return d->currentSize; + + const int currentRow = MimerCurrentRow(d->statementhandle); + MimerFetchScroll(d->statementhandle, static_cast<std::int32_t>(MIMER_LAST), 0); + int size = MimerCurrentRow(d->statementhandle); + if (!MIMER_SUCCEEDED(size)) + size = -1; + MimerFetchScroll(d->statementhandle, MIMER_ABSOLUTE, currentRow); + d->currentSize = size; + return size; +} + +int QMimerSQLResult::numRowsAffected() +{ + Q_D(const QMimerSQLResult); + return d->rowsAffected; +} + +QSqlRecord QMimerSQLResult::record() const +{ + Q_D(const QMimerSQLResult); + QSqlRecord rec; + if (!isActive() || !isSelect() || !driver()) + return rec; + QSqlField field; + const int colSize = MimerColumnCount(d->statementhandle); + for (int i = 0; i < colSize; i++) { + wchar_t colName_w[100]; + MimerColumnName(d->statementhandle, static_cast<std::int16_t>(i + 1), colName_w, + sizeof(colName_w) / sizeof(colName_w[0])); + field.setName(QString::fromWCharArray(colName_w)); + const int32_t mType = MimerColumnType(d->statementhandle, static_cast<std::int16_t>(i + 1)); + const QMetaType::Type type = qDecodeMSQLType(mType); + field.setMetaType(QMetaType(type)); + field.setValue(QVariant(field.metaType())); + // field.setPrecision(); Should be implemented once the Mimer API can give this + // information. + // field.setLength(); Should be implemented once the Mimer API can give + // this information. + rec.insert(i, field); + } + return rec; +} + +bool QMimerSQLResult::prepare(const QString &query) +{ + Q_D(QMimerSQLResult); + int32_t err; + if (!driver()) + return false; + if (!d->preparedQuery) + return QSqlResult::prepare(query); + if (query.isEmpty()) + return false; + cleanup(); + const int option = isForwardOnly() ? MIMER_FORWARD_ONLY : MIMER_SCROLLABLE; + err = MimerBeginStatement8(d->drv_d_func()->sessionhandle, query.toUtf8().constData(), option, + &d->statementhandle); + if (err == MIMER_STATEMENT_CANNOT_BE_PREPARED) { + err = MimerExecuteStatement8(d->drv_d_func()->sessionhandle, query.toUtf8().constData()); + if (MIMER_SUCCEEDED(err)) { + d->executedStatement = true; + d->openCursor = false; + d->openStatement = false; + return true; + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(QCoreApplication::translate("QMimerSQLResult", + "Could not prepare/execute statement"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + d->openStatement = true; + return true; +} + +bool QMimerSQLResult::exec() +{ + Q_D(QMimerSQLResult); + int32_t err; + if (!driver()) + return false; + if (!d->preparedQuery) + return QSqlResult::exec(); + if (d->executedStatement) { + d->executedStatement = false; + return true; + } + if (d->openCursor) { + setAt(QSql::BeforeFirstRow); + err = MimerCloseCursor(d->statementhandle); + d->openCursor = false; + d->currentSize = -1; + } + QVector<QVariant> &values = boundValues(); + if (d->execBatch) + values = d->batch_vector; + int mimParamCount = MimerParameterCount(d->statementhandle); + if (!MIMER_SUCCEEDED(mimParamCount)) + mimParamCount = 0; + if (mimParamCount != values.size()) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Wrong number of parameters"), + genericError, QSqlError::StatementError, nullptr)); + return false; + } + for (int i = 0; i < mimParamCount; i++) { + if (bindValueType(i) == QSql::Out) { + d->callWithOut = true; + continue; + } + const QVariant &val = values.at(i); + if (QSqlResultPrivate::isVariantNull(val) || val.isNull() || val.toString().isNull()) { + err = MimerSetNull(d->statementhandle, i + 1); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("null", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + continue; + } + + const int mimParamType = MimerParameterType(d->statementhandle, i + 1); + const MimerColumnTypes mimDataType = mimerMapColumnTypes(mimParamType); + switch (mimDataType) { + case MimerColumnTypes::Int: { + bool convertOk; + err = MimerSetInt32(d->statementhandle, i + 1, val.toInt(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("int32", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Long: { + bool convertOk; + err = MimerSetInt64(d->statementhandle, i + 1, val.toLongLong(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("int64", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Float: { + bool convertOk; + err = MimerSetFloat(d->statementhandle, i + 1, val.toFloat(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("float", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Double: { + bool convertOk; + err = MimerSetDouble(d->statementhandle, i + 1, val.toDouble(&convertOk)); + if (!convertOk || !MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("double", i), + convertOk ? err : genericError, QSqlError::StatementError, + convertOk ? d->drv_d_func() : nullptr)); + return false; + } + break; + } + case MimerColumnTypes::Binary: { + const QByteArray binArr = val.toByteArray(); + size_t size = static_cast<std::size_t>(binArr.size()); + err = MimerSetBinary(d->statementhandle, i + 1, binArr.data(), size); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("binary", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Boolean: { + err = MimerSetBoolean(d->statementhandle, i + 1, val.toBool() == true ? 1 : 0); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("boolean", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Uuid: { + const QByteArray uuidArray = + QByteArray::fromHex(val.toUuid().toString(QUuid::WithoutBraces).toLatin1()); + const unsigned char *uuid = + reinterpret_cast<const unsigned char *>(uuidArray.constData()); + err = MimerSetUUID(d->statementhandle, i + 1, uuid); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("UUID", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Numeric: + case MimerColumnTypes::String: { + QByteArray string_b = val.toString().trimmed().toUtf8(); + const char *string_u = string_b.constData(); + err = MimerSetString8(d->statementhandle, i + 1, string_u); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet( + mimDataType == MimerColumnTypes::Numeric ? "numeric" : "string", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Date: { + err = MimerSetString8(d->statementhandle, i + 1, val.toString().toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("date", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Time: { + QString timeFormatString = "hh:mm:ss"_L1; + const QTime timeVal = val.toTime(); + if (timeVal.msec() > 0) + timeFormatString.append(".zzz"_L1); + err = MimerSetString8(d->statementhandle, i + 1, + timeVal.toString(timeFormatString).toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("time", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Timestamp: { + QString dateTimeFormatString = "yyyy-MM-dd hh:mm:ss"_L1; + const QDateTime dateTimeVal = val.toDateTime(); + if (dateTimeVal.time().msec() > 0) + dateTimeFormatString.append(".zzz"_L1); + err = MimerSetString8( + d->statementhandle, i + 1, + val.toDateTime().toString(dateTimeFormatString).toUtf8().constData()); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("datetime", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Blob: { + QByteArray blobArr = val.toByteArray(); + const char *blobData = blobArr.constData(); + qsizetype size = blobArr.size(); + err = MimerSetLob(d->statementhandle, i + 1, size, &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + qsizetype maxSize = lobChunkMaxSizeSet; + if (size > maxSize) { + qsizetype left_to_send = size; + for (qsizetype k = 0; left_to_send > 0; k++) { + if (left_to_send <= maxSize) { + err = MimerSetBlobData(&d->lobhandle, &blobData[k * maxSize], + left_to_send); + left_to_send = 0; + } else { + err = MimerSetBlobData(&d->lobhandle, &blobData[k * maxSize], maxSize); + left_to_send = left_to_send - maxSize; + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("BLOB byte array", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } else { + err = MimerSetBlobData(&d->lobhandle, blobArr, size); + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("BLOB byte array", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Clob: { + QByteArray string_b = val.toString().trimmed().toUtf8(); + const char *string_u = string_b.constData(); + size_t size_c = 1; + size_t size = 0; + while (string_u[size++]) + if ((string_u[size] & 0xc0) != 0x80) + size_c++; + err = MimerSetLob(d->statementhandle, i + 1, size_c, &d->lobhandle); + if (MIMER_SUCCEEDED(err)) { + constexpr size_t maxSize = lobChunkMaxSizeSet; + if (size > maxSize) { + size_t left_to_send = size; + size_t pos = 0; + uint step_back = 0; + while (left_to_send > 0 && step_back < maxSize) { + step_back = 0; + if (left_to_send <= maxSize) { + err = MimerSetNclobData8(&d->lobhandle, &string_u[pos], left_to_send); + left_to_send = 0; + } else { + // Check that we don't split a multi-byte utf-8 characters + while (pos + maxSize - step_back > 0 + && (string_u[pos + maxSize - step_back] & 0xc0) == 0x80) + step_back++; + err = MimerSetNclobData8(&d->lobhandle, &string_u[pos], + maxSize - step_back); + left_to_send = left_to_send - maxSize + step_back; + pos += maxSize - step_back; + } + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError(msgCouldNotSet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + } + } else { + err = MimerSetNclobData8(&d->lobhandle, string_u, size); + } + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(msgCouldNotSet("CLOB", i), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + break; + } + case MimerColumnTypes::Unknown: + default: + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Unknown datatype, parameter %1") + .arg(i), + genericError, QSqlError::StatementError, nullptr)); + return false; + } + } + if (d->execBatch) + return true; + err = MimerExecute(d->statementhandle); + if (MIMER_SUCCEEDED(err)) { + d->rowsAffected = err; + int k = 0; + for (qsizetype i = 0; i < values.size(); i++) { + if (bindValueType(i) == QSql::Out || bindValueType(i) == QSql::InOut) { + bindValue(i, data(k), QSql::In); + k++; + } + } + d->callWithOut = false; + } + setSelect(false); + if (MIMER_SEQUENCE_ERROR == err) { + err = MimerOpenCursor(d->statementhandle); + d->rowsAffected = err; + d->openCursor = true; + d->currentRow = QSql::BeforeFirstRow; + setSelect(true); + } + if (!MIMER_SUCCEEDED(err)) { + setLastError( + qMakeError(QCoreApplication::translate("QMimerSQLResult", + "Could not execute statement/open cursor"), + err, QSqlError::StatementError, d->drv_d_func())); + return false; + } + setActive(true); + return true; +} + +bool QMimerSQLResult::execBatch(bool arrayBind) +{ + Q_D(QMimerSQLResult); + Q_UNUSED(arrayBind); + int32_t err; + const QVector<QVariant> values = boundValues(); + + // Check that we only have input parameters. Currently + // we can only handle batch operations without output parameters. + for (qsizetype i = 0; i < values.first().toList().size(); i++) + if (bindValueType(i) == QSql::Out || bindValueType(i) == QSql::InOut) { + setLastError(qMakeError(QCoreApplication::translate( + "QMimerSQLResult", + "Only input parameters can be used in batch operations"), + genericError, QSqlError::StatementError, nullptr)); + d->execBatch = false; + return false; + } + d->execBatch = true; + for (qsizetype i = 0; i < values.first().toList().size(); i++) { + for (qsizetype j = 0; j < values.size(); j++) + d->batch_vector.append(values.at(j).toList().at(i)); + exec(); + if (i != (values.at(0).toList().size() - 1)) { + err = MimerAddBatch(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + //: %1 is the batch number + QCoreApplication::translate("QMimerSQLResult", "Could not add batch %1") + .arg(i), + err, QSqlError::StatementError, d->drv_d_func())); + d->execBatch = false; + return false; + } + } + d->batch_vector.clear(); + } + d->execBatch = false; + err = MimerExecute(d->statementhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLResult", "Could not execute batch"), err, + QSqlError::StatementError, d->drv_d_func())); + return false; + } + return true; +} + +QVariant QMimerSQLResult::lastInsertId() const +{ + Q_D(const QMimerSQLResult); + int64_t lastSequence; + const int32_t err = MimerGetSequenceInt64(d->statementhandle, &lastSequence); + if (!MIMER_SUCCEEDED(err)) + return QVariant(QMetaType(QMetaType::LongLong), nullptr); + return QVariant(qint64(lastSequence)); +} + +bool QMimerSQLDriver::hasFeature(DriverFeature f) const +{ + switch (f) { + case NamedPlaceholders: // Is true in reality but Qt parses Sql statement... + case EventNotifications: + case LowPrecisionNumbers: + case MultipleResultSets: + case SimpleLocking: + case CancelQuery: + return false; + case FinishQuery: + case LastInsertId: + case Transactions: + case QuerySize: + case BLOB: + case Unicode: + case PreparedQueries: + case PositionalPlaceholders: + case BatchOperations: + return true; + } + return true; +} + +bool QMimerSQLDriver::open(const QString &db, const QString &user, const QString &password, + const QString &host, int port, const QString &connOpts) +{ + Q_D(QMimerSQLDriver); + Q_UNUSED(host); + Q_UNUSED(port); + Q_UNUSED(connOpts); + if (isOpen()) + close(); + const int32_t err = MimerBeginSession8(db.toUtf8().constData(), user.toUtf8().constData(), + password.toUtf8().constData(), &d->sessionhandle); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not connect to database") + + " "_L1 + db, + err, QSqlError::ConnectionError, nullptr)); + setOpenError(true); + return false; + } + d->dbUser = user; + d->dbName = db; + setOpen(true); + setOpenError(false); + return true; +} + +void QMimerSQLDriver::close() +{ + Q_D(QMimerSQLDriver); + if (isOpen()) { + const int end_err = MimerEndSession(&d->sessionhandle); + if (MIMER_SUCCEEDED(end_err)) { + setOpen(false); + setOpenError(false); + } + } +} + +QSqlResult *QMimerSQLDriver::createResult() const +{ + return new QMimerSQLResult(this); +} + +QStringList QMimerSQLDriver::tables(QSql::TableType type) const +{ + QStringList tl; + if (!isOpen()) + return tl; + QSqlQuery t(createResult()); + QString sql; + switch (type) { + case QSql::Tables: { + sql = "select table_name from information_schema.tables where " + "table_type=\'BASE TABLE\' AND table_schema = CURRENT_USER"_L1; + break; + } + case QSql::SystemTables: { + sql = "select table_name from information_schema.tables where " + "table_type=\'BASE TABLE\' AND table_schema = \'SYSTEM\'"_L1; + break; + } + case QSql::Views: { + sql = "select table_name from information_schema.tables where " + "table_type=\'VIEW\' AND table_schema = CURRENT_USER"_L1; + break; + } + case QSql::AllTables: { + sql = "select table_name from information_schema.tables where " + "(table_type=\'VIEW\' or table_type=\'BASE TABLE\')" + " AND (table_schema = CURRENT_USER OR table_schema =\'SYSTEM\')"_L1; + break; + } + default: + break; + } + if (sql.length() > 0) { + t.exec(sql); + while (t.next()) + tl.append(t.value(0).toString()); + } + return tl; +} + +QSqlIndex QMimerSQLDriver::primaryIndex(const QString &tablename) const +{ + Q_D(const QMimerSQLDriver); + if (!isOpen()) + return QSqlIndex(); + QString table = tablename; + if (isIdentifierEscaped(table, QSqlDriver::TableName)) + table = stripDelimiters(table, QSqlDriver::TableName); + QSqlIndex index(tablename); + QSqlQuery t(createResult()); + QString schema; + QString qualifiedName = table; + d->splitTableQualifier(qualifiedName, &schema, &table); + QString sql = + "select information_schema.ext_access_paths.column_name," + "case when data_type = 'INTERVAL' then 'INTERVAL '|| interval_type " + "when data_type = 'INTEGER' and numeric_precision > 10 then 'BIGINT' " + "when data_type = 'INTEGER' and numeric_precision <= 10 AND NUMERIC_PRECISION > 5 " + "then 'INTEGER' when data_type = 'INTEGER' and numeric_precision <= 5 then 'SMALLINT' " + "else upper(data_type) end as data_type " + "from information_schema.ext_access_paths full outer join " + "information_schema.columns on information_schema.ext_access_paths.column_name = " + "information_schema.columns.column_name and " + "information_schema.ext_access_paths.table_name = " + "information_schema.columns.table_name where " + "information_schema.ext_access_paths.table_name = \'"_L1; + sql.append(table) + .append("\' and index_type = \'PRIMARY KEY\'"_L1); + if (schema.length() == 0) + sql.append(" and table_schema = CURRENT_USER"_L1); + else + sql.append(" and table_schema = \'"_L1).append(schema).append("\'"_L1); + + if (!t.exec(sql)) + return QSqlIndex(); + int i = 0; + while (t.next()) { + QSqlField field(t.value(0).toString(), + QMetaType(qDecodeMSQLType(qLookupMimDataType(t.value(1).toString()))), + tablename); + index.insert(i, field); + index.setName(t.value(0).toString()); + i++; + } + return index; +} + +QSqlRecord QMimerSQLDriver::record(const QString &tablename) const +{ + Q_D(const QMimerSQLDriver); + if (!isOpen()) + return QSqlRecord(); + QSqlRecord rec; + QSqlQuery t(createResult()); + QString qualifiedName = tablename; + if (isIdentifierEscaped(qualifiedName, QSqlDriver::TableName)) + qualifiedName = stripDelimiters(qualifiedName, QSqlDriver::TableName); + QString schema, table; + d->splitTableQualifier(qualifiedName, &schema, &table); + + QString sql = + "select column_name, case when data_type = 'INTERVAL' then 'INTERVAL '|| interval_type " + "when data_type = 'INTEGER' and numeric_precision > 10 then 'BIGINT' " + "when data_type = 'INTEGER' and numeric_precision <= 10 AND numeric_precision > 5 " + "then 'INTEGER' when data_type = 'INTEGER' and numeric_precision <= 5 then 'SMALLINT' " + "else UPPER(data_type) end as data_type, case when is_nullable = 'YES' then false else " + "true end as required, " + "coalesce(numeric_precision, coalesce(datetime_precision,coalesce(interval_precision, " + "-1))) as prec from information_schema.columns where table_name = \'"_L1; + if (schema.length() == 0) + sql.append(table).append("\' and table_schema = CURRENT_USER"_L1); + else + sql.append(table).append("\' and table_schema = \'"_L1).append(schema).append("\'"_L1); + sql.append(" order by ordinal_position"_L1); + if (!t.exec(sql)) + return QSqlRecord(); + + while (t.next()) { + QSqlField field(t.value(0).toString(), + QMetaType(qDecodeMSQLType(qLookupMimDataType(t.value(1).toString()))), + tablename); + field.setRequired(t.value(3).toBool()); + if (t.value(3).toInt() != -1) + field.setPrecision(t.value(3).toInt()); + rec.append(field); + } + + return rec; +} + +QVariant QMimerSQLDriver::handle() const +{ + Q_D(const QMimerSQLDriver); + return QVariant::fromValue(d->sessionhandle); +} + +QString QMimerSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const +{ + Q_UNUSED(type); + QString res = identifier; + if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) { + res.replace(u'"', "\"\""_L1); + res = u'"' + res + u'"'; + res.replace(u'.', "\".\""_L1); + } + return res; +} + +bool QMimerSQLDriver::beginTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerBeginTransaction(d->sessionhandle, MIMER_TRANS_READWRITE); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not start transaction"), err, + QSqlError::TransactionError, d)); + return false; + } + return true; +} + +bool QMimerSQLDriver::commitTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerEndTransaction(d->sessionhandle, MIMER_COMMIT); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not commit transaction"), err, + QSqlError::TransactionError, d)); + return false; + } + return true; +} + +bool QMimerSQLDriver::rollbackTransaction() +{ + Q_D(const QMimerSQLDriver); + const int32_t err = MimerEndTransaction(d->sessionhandle, MIMER_ROLLBACK); + if (!MIMER_SUCCEEDED(err)) { + setLastError(qMakeError( + QCoreApplication::translate("QMimerSQLDriver", "Could not roll back transaction"), + err, QSqlError::TransactionError, d)); + return false; + } + return true; +} + +void QMimerSQLDriverPrivate::splitTableQualifier(const QString &qualifiedName, QString *schema, + QString *table) const +{ + const QList<QStringView> l = QStringView(qualifiedName).split(u'.'); + int n = l.count(); + if (n > 2) { + return; // can't possibly be a valid table qualifier + } else if (n == 1) { + *schema = QString(); + *table = l.at(0).toString(); + } else { + *schema = l.at(0).toString(); + *table = l.at(1).toString(); + } +} + +QT_END_NAMESPACE + +#include "moc_qsql_mimer.cpp" diff --git a/src/plugins/sqldrivers/mimer/qsql_mimer.h b/src/plugins/sqldrivers/mimer/qsql_mimer.h new file mode 100644 index 0000000000..03ad8f21f2 --- /dev/null +++ b/src/plugins/sqldrivers/mimer/qsql_mimer.h @@ -0,0 +1,50 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// Copyright (C) 2022 Mimer Information Technology +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QSQL_MIMER_H +#define QSQL_MIMER_H + +#include <QtSql/qsqldriver.h> +#include <QUuid> +#include <mimerapi.h> + +#ifdef QT_PLUGIN +# define Q_EXPORT_SQLDRIVER_MIMER +#else +# define Q_EXPORT_SQLDRIVER_MIMER Q_SQL_EXPORT +#endif + +QT_BEGIN_NAMESPACE + +class QMimerSQLDriverPrivate; + +class Q_EXPORT_SQLDRIVER_MIMER QMimerSQLDriver : public QSqlDriver +{ + friend class QMimerSQLResultPrivate; + Q_DECLARE_PRIVATE(QMimerSQLDriver) + Q_OBJECT +public: + explicit QMimerSQLDriver(QObject *parent = nullptr); + explicit QMimerSQLDriver(MimerSession *conn, QObject *parent = nullptr); + ~QMimerSQLDriver() override; + bool hasFeature(DriverFeature f) const override; + bool open(const QString &db, const QString &user, const QString &password, const QString &host, + int port, const QString &connOpts) override; + void close() override; + QSqlResult *createResult() const override; + QStringList tables(QSql::TableType type) const override; + QSqlIndex primaryIndex(const QString &tablename) const override; + QSqlRecord record(const QString &tablename) const override; + QVariant handle() const override; + QString escapeIdentifier(const QString &identifier, IdentifierType type) const override; +protected: + bool beginTransaction() override; + bool commitTransaction() override; + bool rollbackTransaction() override; + +private: +}; + +QT_END_NAMESPACE + +#endif // QSQL_MIMER diff --git a/src/plugins/sqldrivers/mysql/CMakeLists.txt b/src/plugins/sqldrivers/mysql/CMakeLists.txt index 4a65847be7..2e3d028584 100644 --- a/src/plugins/sqldrivers/mysql/CMakeLists.txt +++ b/src/plugins/sqldrivers/mysql/CMakeLists.txt @@ -1,8 +1,6 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from mysql.pro. - ##################################################################### ## QMYSQLDriverPlugin Plugin: ##################################################################### @@ -16,6 +14,7 @@ qt_internal_add_plugin(QMYSQLDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES MySQL::MySQL Qt::Core @@ -23,7 +22,4 @@ qt_internal_add_plugin(QMYSQLDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:mysql.pro:<TRUE>: -# OTHER_FILES = "mysql.json" - qt_internal_force_macos_intel_arch(QMYSQLDriverPlugin) diff --git a/src/plugins/sqldrivers/mysql/qsql_mysql.cpp b/src/plugins/sqldrivers/mysql/qsql_mysql.cpp index 717692b772..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 @@ -198,6 +237,7 @@ static QMetaType qDecodeMYSQLType(enum_field_types mysqltype, uint flags) case MYSQL_TYPE_YEAR: type = QMetaType::Int; break; + case MYSQL_TYPE_BIT: case MYSQL_TYPE_LONGLONG: type = (flags & UNSIGNED_FLAG) ? QMetaType::ULongLong : QMetaType::LongLong; break; @@ -248,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; } @@ -286,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) { @@ -330,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 { @@ -384,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; } @@ -502,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(); } @@ -519,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) @@ -528,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) @@ -537,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 @@ -552,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) @@ -660,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); } @@ -777,6 +840,7 @@ bool QMYSQLResult::nextResult() 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; } } @@ -789,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); @@ -844,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; @@ -864,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; @@ -877,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); @@ -902,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: @@ -961,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)); @@ -1032,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 @@ -1139,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) @@ -1162,6 +1222,47 @@ static bool setOptionBool(MYSQL *mysql, mysql_option option, QStringView v) 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, @@ -1199,18 +1300,27 @@ bool QMYSQLDriver::open(const QString &db, {"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)) { - 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; } @@ -1241,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); } @@ -1294,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())) { @@ -1307,6 +1418,10 @@ 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(); @@ -1326,6 +1441,7 @@ void QMYSQLDriver::close() #endif mysql_close(d->mysql); d->mysql = nullptr; + d->dbName.clear(); setOpen(false); setOpenError(false); } @@ -1342,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()) @@ -1382,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 @@ -1408,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")) { @@ -1423,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")) { @@ -1438,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")) { @@ -1475,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: @@ -1484,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' + diff --git a/src/plugins/sqldrivers/oci/CMakeLists.txt b/src/plugins/sqldrivers/oci/CMakeLists.txt index eb0597e434..66c4219905 100644 --- a/src/plugins/sqldrivers/oci/CMakeLists.txt +++ b/src/plugins/sqldrivers/oci/CMakeLists.txt @@ -1,8 +1,6 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from oci.pro. - ##################################################################### ## QOCIDriverPlugin Plugin: ##################################################################### @@ -23,9 +21,6 @@ qt_internal_add_plugin(QOCIDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:oci.pro:<TRUE>: -# OTHER_FILES = "oci.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/oci/qsql_oci.cpp b/src/plugins/sqldrivers/oci/qsql_oci.cpp index b8202a64cc..68e303490d 100644 --- a/src/plugins/sqldrivers/oci/qsql_oci.cpp +++ b/src/plugins/sqldrivers/oci/qsql_oci.cpp @@ -7,6 +7,7 @@ #include <qdatetime.h> #include <qdebug.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qmetatype.h> #if QT_CONFIG(regularexpression) #include <qregularexpression.h> @@ -30,14 +31,7 @@ #define _int64 __int64 #endif - #include <oci.h> -#ifdef max -#undef max -#endif -#ifdef min -#undef min -#endif #include <stdlib.h> @@ -58,6 +52,8 @@ Q_DECLARE_METATYPE(OCIStmt*) QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcOci, "qt.sql.oci") + using namespace Qt::StringLiterals; #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN @@ -140,8 +136,8 @@ QOCIDateTime::QOCIDateTime(OCIEnv *env, OCIError *err, const QDateTime &dt) if (dt.isValid()) { const QDate date = dt.date(); const QTime time = dt.time(); - // Zone in +hh:mm format (stripping UTC prefix from OffsetName) - QString timeZone = dt.timeZone().displayName(dt, QTimeZone::OffsetName).mid(3); + // Zone in +hh:mm format + const QString timeZone = dt.toString("ttt"_L1); const OraText *tz = reinterpret_cast<const OraText *>(timeZone.utf16()); OCIDateTimeConstruct(env, err, dateTime, date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000000, @@ -280,7 +276,7 @@ public: 0); #ifdef QOCI_DEBUG if (r != 0) - qWarning("QOCIResultPrivate::setCharset: Couldn't set OCI_ATTR_CHARSET_FORM."); + qCWarning(lcOci, "QOCIResultPrivate::setCharset: Couldn't set OCI_ATTR_CHARSET_FORM."); #endif #endif @@ -440,7 +436,7 @@ int QOCIResultPrivate::bindValue(OCIStmt *sql, OCIBind **hbnd, OCIError *err, in -1, SQLT_RDD, indPtr, 0, 0, 0, 0, OCI_DEFAULT); } else { - qWarning("Unknown bind variable"); + qCWarning(lcOci, "Unknown bind variable"); r = OCI_ERROR; } } else { @@ -556,7 +552,7 @@ void QOCIDriverPrivate::allocErrorHandle() OCI_HTYPE_ERROR, 0, nullptr); if (r != OCI_SUCCESS) - qWarning("QOCIDriver: unable to allocate error handle"); + qCWarning(lcOci, "QOCIDriver: unable to allocate error handle"); } struct OraFieldInfo @@ -592,12 +588,7 @@ QString qOraWarn(OCIError *err, int *errorCode) void qOraWarning(const char* msg, OCIError *err) { -#ifdef QOCI_DEBUG - qWarning("%s %s", msg, qPrintable(qOraWarn(err))); -#else - Q_UNUSED(msg); - Q_UNUSED(err); -#endif + qCWarning(lcOci, "%s %ls", msg, qUtf16Printable(qOraWarn(err))); } static int qOraErrorNumber(OCIError *err) @@ -660,7 +651,7 @@ QMetaType qDecodeOCIType(const QString& ocitype, QSql::NumericalPrecisionPolicy else if (ocitype == "UNDEFINED"_L1) type = QMetaType::UnknownType; if (type == QMetaType::UnknownType) - qWarning("qDecodeOCIType: unknown type: %s", ocitype.toLocal8Bit().constData()); + qCWarning(lcOci, "qDecodeOCIType: unknown type: %ls", qUtf16Printable(ocitype)); return QMetaType(type); } @@ -728,7 +719,7 @@ QMetaType qDecodeOCIType(int ocitype, QSql::NumericalPrecisionPolicy precisionPo type = QMetaType::QDateTime; break; default: - qWarning("qDecodeOCIType: unknown OCI datatype: %d", ocitype); + qCWarning(lcOci, "qDecodeOCIType: unknown OCI datatype: %d", ocitype); break; } return QMetaType(type); @@ -745,7 +736,6 @@ static QSqlField qFromOraInf(const OraFieldInfo &ofi) f.setLength(ofi.oraPrecision == 0 ? 38 : int(ofi.oraPrecision)); f.setPrecision(ofi.oraScale); - f.setSqlType(int(ofi.oraType)); return f; } @@ -844,7 +834,7 @@ QOCICols::OraFieldInf::~OraFieldInf() if (lob) { int r = OCIDescriptorFree(lob, OCI_DTYPE_LOB); if (r != 0) - qWarning("QOCICols: Cannot free LOB descriptor"); + qCWarning(lcOci, "QOCICols: Cannot free LOB descriptor"); } if (dataPtr) { switch (typ.id()) { @@ -853,7 +843,7 @@ QOCICols::OraFieldInf::~OraFieldInf() case QMetaType::QDateTime: { int r = OCIDescriptorFree(dataPtr, OCI_DTYPE_TIMESTAMP_TZ); if (r != OCI_SUCCESS) - qWarning("QOCICols: Cannot free OCIDateTime descriptor"); + qCWarning(lcOci, "QOCICols: Cannot free OCIDateTime descriptor"); break; } default: @@ -908,7 +898,7 @@ QOCICols::QOCICols(int size, QOCIResultPrivate* dp) case QMetaType::QDateTime: r = OCIDescriptorAlloc(d->env, (void **)&fieldInf[idx].dataPtr, OCI_DTYPE_TIMESTAMP_TZ, 0, 0); if (r != OCI_SUCCESS) { - qWarning("QOCICols: Unable to allocate the OCIDateTime descriptor"); + qCWarning(lcOci, "QOCICols: Unable to allocate the OCIDateTime descriptor"); break; } r = OCIDefineByPos(d->sql, @@ -1079,7 +1069,7 @@ OCILobLocator **QOCICols::createLobLocator(int position, OCIEnv* env) 0, 0); if (r != 0) { - qWarning("QOCICols: Cannot create LOB locator"); + qCWarning(lcOci, "QOCICols: Cannot create LOB locator"); lob = 0; } return &lob; @@ -1317,7 +1307,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a return false; #ifdef QOCI_DEBUG - qDebug() << "columnCount:" << columnCount << boundValues; + qCDebug(lcOci) << "columnCount:" << columnCount << boundValues; #endif int i; @@ -1436,7 +1426,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a // we may now populate column with data for (uint row = 0; row < col.recordCount; ++row) { - const QVariant &val = boundValues.at(i).toList().at(row); + const QVariant val = boundValues.at(i).toList().at(row); if (QSqlResultPrivate::isVariantNull(val) && !d->isOutValue(i)) { columns[i].indicators[row] = -1; @@ -1514,13 +1504,13 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a QOCIBatchColumn &bindColumn = columns[i]; #ifdef QOCI_DEBUG - qDebug("OCIBindByPos(%p, %p, %p, %d, %p, %d, %d, %p, %p, 0, %d, %p, OCI_DEFAULT)", + qCDebug(lcOci, "OCIBindByPos(%p, %p, %p, %d, %p, %d, %d, %p, %p, 0, %d, %p, OCI_DEFAULT)", d->sql, &bindColumn.bindh, d->err, i + 1, bindColumn.data, bindColumn.maxLen, bindColumn.bindAs, bindColumn.indicators, bindColumn.lengths, arrayBind ? bindColumn.maxarr_len : 0, arrayBind ? &bindColumn.curelep : 0); for (int ii = 0; ii < (int)bindColumn.recordCount; ++ii) { - qDebug(" record %d: indicator %d, length %d", ii, bindColumn.indicators[ii], + qCDebug(lcOci, " record %d: indicator %d, length %d", ii, bindColumn.indicators[ii], bindColumn.lengths[ii]); } #endif @@ -1540,7 +1530,7 @@ bool QOCICols::execBatch(QOCIResultPrivate *d, QVariantList &boundValues, bool a OCI_DEFAULT); #ifdef QOCI_DEBUG - qDebug("After OCIBindByPos: r = %d, bindh = %p", r, bindColumn.bindh); + qCDebug(lcOci, "After OCIBindByPos: r = %d, bindh = %p", r, bindColumn.bindh); #endif if (r != OCI_SUCCESS && r != OCI_SUCCESS_WITH_INFO) { @@ -1800,7 +1790,7 @@ void QOCICols::getValues(QVariantList &v, int index) v[index + i] = QVariant(QMetaType(QMetaType::QByteArray)); break; default: - qWarning("QOCICols::value: unknown data type"); + qCWarning(lcOci, "QOCICols::value: unknown data type"); break; } } @@ -1821,7 +1811,7 @@ QOCIResultPrivate::QOCIResultPrivate(QOCIResult *q, const QOCIDriver *drv) OCI_HTYPE_ERROR, 0, nullptr); if (r != OCI_SUCCESS) - qWarning("QOCIResult: unable to alloc error handle"); + qCWarning(lcOci, "QOCIResult: unable to alloc error handle"); } QOCIResultPrivate::~QOCIResultPrivate() @@ -1829,10 +1819,10 @@ QOCIResultPrivate::~QOCIResultPrivate() delete cols; if (sql && OCIHandleFree(sql, OCI_HTYPE_STMT) != OCI_SUCCESS) - qWarning("~QOCIResult: unable to free statement handle"); + qCWarning(lcOci, "~QOCIResult: unable to free statement handle"); if (OCIHandleFree(err, OCI_HTYPE_ERROR) != OCI_SUCCESS) - qWarning("~QOCIResult: unable to free error report handle"); + qCWarning(lcOci, "~QOCIResult: unable to free error report handle"); } @@ -1889,7 +1879,7 @@ bool QOCIResult::gotoNext(QSqlCachedResult::ValueCache &values, int index) break; case OCI_ERROR: if (qOraErrorNumber(d->err) == 1406) { - qWarning("QOCI Warning: data truncated for %s", lastQuery().toLocal8Bit().constData()); + qCWarning(lcOci, "QOCI Warning: data truncated for %ls", qUtf16Printable(lastQuery())); r = OCI_SUCCESS; /* ignore it */ break; } @@ -2003,7 +1993,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to get statement type"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2018,7 +2008,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to bind value"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2037,7 +2027,7 @@ bool QOCIResult::exec() setLastError(qMakeError(QCoreApplication::translate("QOCIResult", "Unable to execute statement"), QSqlError::StatementError, d->err)); #ifdef QOCI_DEBUG - qDebug() << "lastQuery()" << lastQuery(); + qCDebug(lcOci) << "lastQuery()" << lastQuery(); #endif return false; } @@ -2129,7 +2119,7 @@ QOCIDriver::QOCIDriver(QObject* parent) 0, NULL); if (r != 0) { - qWarning("QOCIDriver: unable to create environment"); + qCWarning(lcOci, "QOCIDriver: unable to create environment"); setLastError(qMakeError(tr("Unable to initialize", "QOCIDriver"), QSqlError::ConnectionError, d->err)); return; @@ -2160,10 +2150,10 @@ QOCIDriver::~QOCIDriver() close(); int r = OCIHandleFree(d->err, OCI_HTYPE_ERROR); if (r != OCI_SUCCESS) - qWarning("Unable to free Error handle: %d", r); + qCWarning(lcOci, "Unable to free Error handle: %d", r); r = OCIHandleFree(d->env, OCI_HTYPE_ENV); if (r != OCI_SUCCESS) - qWarning("Unable to free Environment handle: %d", r); + qCWarning(lcOci, "Unable to free Environment handle: %d", r); } bool QOCIDriver::hasFeature(DriverFeature f) const @@ -2198,8 +2188,8 @@ static void qParseOpts(const QString &options, QOCIDriverPrivate *d) for (const auto tmp : opts) { qsizetype idx; if ((idx = tmp.indexOf(u'=')) == -1) { - qWarning("QOCIDriver::parseArgs: Invalid parameter: '%s'", - tmp.toLocal8Bit().constData()); + qCWarning(lcOci, "QOCIDriver::parseArgs: Invalid parameter: '%ls'", + qUtf16Printable(tmp.toString())); continue; } const QStringView opt = tmp.left(idx); @@ -2219,12 +2209,12 @@ static void qParseOpts(const QString &options, QOCIDriverPrivate *d) } else if (val == "OCI_SYSOPER"_L1) { d->authMode = OCI_SYSOPER; } else if (val != "OCI_DEFAULT"_L1) { - qWarning("QOCIDriver::parseArgs: Unsupported value for OCI_AUTH_MODE: '%s'", - val.toLocal8Bit().constData()); + qCWarning(lcOci, "QOCIDriver::parseArgs: Unsupported value for OCI_AUTH_MODE: '%ls'", + qUtf16Printable(val.toString())); } } else { - qWarning("QOCIDriver::parseArgs: Invalid parameter: '%s'", - opt.toLocal8Bit().constData()); + qCWarning(lcOci, "QOCIDriver::parseArgs: Invalid parameter: '%ls'", + qUtf16Printable(opt.toString())); } } } @@ -2321,7 +2311,7 @@ bool QOCIDriver::open(const QString & db, sizeof(vertxt), OCI_HTYPE_SVCCTX); if (r != 0) { - qWarning("QOCIDriver::open: could not get Oracle server version."); + qCWarning(lcOci, "QOCIDriver::open: could not get Oracle server version."); } else { QString versionStr; versionStr = QString(reinterpret_cast<const QChar *>(vertxt)); @@ -2370,7 +2360,7 @@ bool QOCIDriver::beginTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::beginTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::beginTransaction: Database not open"); return false; } int r = OCITransStart(d->svc, @@ -2391,7 +2381,7 @@ bool QOCIDriver::commitTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::commitTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::commitTransaction: Database not open"); return false; } int r = OCITransCommit(d->svc, @@ -2411,7 +2401,7 @@ bool QOCIDriver::rollbackTransaction() { Q_D(QOCIDriver); if (!isOpen()) { - qWarning("QOCIDriver::rollbackTransaction: Database not open"); + qCWarning(lcOci, "QOCIDriver::rollbackTransaction: Database not open"); return false; } int r = OCITransRollback(d->svc, @@ -2563,8 +2553,7 @@ QSqlRecord QOCIDriver::record(const QString& tablename) const // eg. a sub-query on the sys.synonyms table QString stmt("select column_name, data_type, data_length, " "data_precision, data_scale, nullable, data_default%1" - "from all_tab_columns a " - "where a.table_name=%2"_L1); + "from all_tab_columns a "_L1); if (d->serverVersion >= 9) stmt = stmt.arg(", char_length "_L1); else @@ -2578,7 +2567,7 @@ QSqlRecord QOCIDriver::record(const QString& tablename) const else table = table.toUpper(); - tmpStmt = stmt.arg(u'\'' + table + u'\''); + tmpStmt = stmt + "where a.table_name='"_L1 + table + u'\''; if (owner.isEmpty()) { owner = d->user; } @@ -2692,7 +2681,7 @@ QSqlIndex QOCIDriver::primaryIndex(const QString& tablename) const QString QOCIDriver::formatValue(const QSqlField &field, bool trimStrings) const { - switch (field.typeID()) { + switch (field.metaType().id()) { case QMetaType::QDateTime: { QDateTime datetime = field.value().toDateTime(); QString datestring; @@ -2754,10 +2743,19 @@ QString QOCIDriver::escapeIdentifier(const QString &identifier, IdentifierType t QString res = identifier; if (!identifier.isEmpty() && !isIdentifierEscaped(identifier, type)) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } +int QOCIDriver::maximumIdentifierLength(IdentifierType type) const +{ + Q_D(const QOCIDriver); + Q_UNUSED(type); + return d->serverVersion > 12 ? 128 : 30; +} + QT_END_NAMESPACE + +#include "moc_qsql_oci_p.cpp" diff --git a/src/plugins/sqldrivers/oci/qsql_oci_p.h b/src/plugins/sqldrivers/oci/qsql_oci_p.h index b2aca6d563..91fef9de7b 100644 --- a/src/plugins/sqldrivers/oci/qsql_oci_p.h +++ b/src/plugins/sqldrivers/oci/qsql_oci_p.h @@ -58,6 +58,7 @@ public: bool trimStrings) const override; QVariant handle() const override; QString escapeIdentifier(const QString &identifier, IdentifierType) const override; + int maximumIdentifierLength(IdentifierType type) const override; protected: bool beginTransaction() override; diff --git a/src/plugins/sqldrivers/odbc/CMakeLists.txt b/src/plugins/sqldrivers/odbc/CMakeLists.txt index 2b56a4fca6..7812aced2c 100644 --- a/src/plugins/sqldrivers/odbc/CMakeLists.txt +++ b/src/plugins/sqldrivers/odbc/CMakeLists.txt @@ -1,13 +1,11 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from odbc.pro. - ##################################################################### ## QODBCDriverPlugin Plugin: ##################################################################### -qt_find_package(ODBC) # special case +qt_find_package(ODBC) qt_internal_add_plugin(QODBCDriverPlugin OUTPUT_NAME qsqlodbc @@ -18,6 +16,7 @@ qt_internal_add_plugin(QODBCDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES ODBC::ODBC Qt::Core @@ -25,9 +24,6 @@ qt_internal_add_plugin(QODBCDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:odbc.pro:<TRUE>: -# OTHER_FILES = "odbc.json" - ## Scopes: ##################################################################### diff --git a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp index 4b7ad455f1..976911d458 100644 --- a/src/plugins/sqldrivers/odbc/qsql_odbc.cpp +++ b/src/plugins/sqldrivers/odbc/qsql_odbc.cpp @@ -10,6 +10,7 @@ #include <qcoreapplication.h> #include <qdatetime.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qmath.h> #include <qsqlerror.h> #include <qsqlfield.h> @@ -26,38 +27,62 @@ QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcOdbc, "qt.sql.odbc") + using namespace Qt::StringLiterals; +// non-standard ODBC SQL data type from SQL Server sometimes used instead of SQL_TIME +#ifndef SQL_SS_TIME2 +#define SQL_SS_TIME2 (-154) +#endif + // undefine this to prevent initial check of the ODBC driver #define ODBC_CHECK_DRIVER -static const int COLNAMESIZE = 256; -static const SQLSMALLINT TABLENAMESIZE = 128; +static constexpr int COLNAMESIZE = 256; +static constexpr SQLSMALLINT TABLENAMESIZE = 128; //Map Qt parameter types to ODBC types -static const SQLSMALLINT qParamType[4] = { SQL_PARAM_INPUT, SQL_PARAM_INPUT, SQL_PARAM_OUTPUT, SQL_PARAM_INPUT_OUTPUT }; +static constexpr SQLSMALLINT qParamType[4] = { SQL_PARAM_INPUT, SQL_PARAM_INPUT, SQL_PARAM_OUTPUT, SQL_PARAM_INPUT_OUTPUT }; -inline static QString fromSQLTCHAR(const QVarLengthArray<SQLTCHAR>& input, qsizetype size=-1) +class SqlStmtHandle { - QString result; +public: + SqlStmtHandle(SQLHANDLE hDbc) + { + SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &stmtHandle); + } + ~SqlStmtHandle() + { + if (stmtHandle != SQL_NULL_HSTMT) + SQLFreeHandle(SQL_HANDLE_STMT, stmtHandle); + } + SQLHANDLE handle() const + { + return stmtHandle; + } + bool isValid() const + { + return stmtHandle != SQL_NULL_HSTMT; + } + SQLHANDLE stmtHandle = SQL_NULL_HSTMT; +}; +template<typename C, int SIZE = sizeof(SQLTCHAR)> +inline static QString fromSQLTCHAR(const C &input, qsizetype size = -1) +{ // Remove any trailing \0 as some drivers misguidedly append one - int realsize = qMin(size, input.size()); - if (realsize > 0 && input[realsize-1] == 0) + qsizetype realsize = qMin(size, input.size()); + if (realsize > 0 && input[realsize - 1] == 0) realsize--; - switch(sizeof(SQLTCHAR)) { - case 1: - result=QString::fromUtf8((const char *)input.constData(), realsize); - break; - case 2: - result = QString::fromUtf16(reinterpret_cast<const char16_t *>(input.constData()), realsize); - break; - case 4: - result = QString::fromUcs4(reinterpret_cast<const char32_t *>(input.constData()), realsize); - break; - default: - qCritical("sizeof(SQLTCHAR) is %d. Don't know how to handle this.", int(sizeof(SQLTCHAR))); - } - return result; + if constexpr (SIZE == 1) + return QString::fromUtf8(reinterpret_cast<const char *>(input.constData()), realsize); + else if constexpr (SIZE == 2) + return QString::fromUtf16(reinterpret_cast<const char16_t *>(input.constData()), realsize); + else if constexpr (SIZE == 4) + return QString::fromUcs4(reinterpret_cast<const char32_t *>(input.constData()), realsize); + else + static_assert(QtPrivate::value_dependent_false<SIZE>(), + "Don't know how to handle sizeof(SQLTCHAR) != 1/2/4"); } template<int SIZE = sizeof(SQLTCHAR)> @@ -74,14 +99,13 @@ QStringConverter::Encoding encodingForSqlTChar() "Don't know how to handle sizeof(SQLTCHAR) != 1/2/4"); } -inline static QVarLengthArray<SQLTCHAR> toSQLTCHAR(const QString &input) +inline static QVarLengthArray<SQLTCHAR> toSQLTCHAR(QStringView input) { QVarLengthArray<SQLTCHAR> result; QStringEncoder enc(encodingForSqlTChar()); result.resize(enc.requiredSpace(input.size())); const auto end = enc.appendToBuffer(reinterpret_cast<char *>(result.data()), input); result.resize((end - reinterpret_cast<char *>(result.data())) / sizeof(SQLTCHAR)); - result.append(0); // make sure it's null terminated, doesn't matter if it already is, it does if it isn't. return result; } @@ -90,7 +114,7 @@ class QODBCDriverPrivate : public QSqlDriverPrivate Q_DECLARE_PUBLIC(QODBCDriver) public: - enum DefaultCase {Lower, Mixed, Upper, Sensitive}; + enum class DefaultCase {Lower, Mixed, Upper, Sensitive}; using QSqlDriverPrivate::QSqlDriverPrivate; SQLHANDLE hEnv = nullptr; @@ -111,15 +135,18 @@ public: void checkHasMultiResults(); void checkSchemaUsage(); void checkDateTimePrecision(); + void checkDefaultCase(); bool setConnectionOptions(const QString& connOpts); void splitTableQualifier(const QString &qualifier, QString &catalog, QString &schema, QString &table) const; - DefaultCase defaultCase() const; QString adjustCase(const QString&) const; QChar quoteChar(); + SQLRETURN sqlFetchNext(const SqlStmtHandle &hStmt) const; + SQLRETURN sqlFetchNext(SQLHANDLE hStmt) const; private: bool isQuoteInitialized = false; QChar quote = u'"'; + DefaultCase m_defaultCase = DefaultCase::Mixed; }; class QODBCResultPrivate; @@ -199,118 +226,121 @@ void QODBCResultPrivate::updateStmtHandleState() disconnectCount = drv_d_func() ? drv_d_func()->disconnectCount : 0; } -static QString qWarnODBCHandle(int handleType, SQLHANDLE handle, int *nativeCode = nullptr) +struct DiagRecord +{ + QString description; + QString sqlState; + QString errorCode; +}; +static QList<DiagRecord> qWarnODBCHandle(int handleType, SQLHANDLE handle) { - SQLINTEGER nativeCode_ = 0; + SQLINTEGER nativeCode = 0; SQLSMALLINT msgLen = 0; + SQLSMALLINT i = 1; SQLRETURN r = SQL_NO_DATA; - SQLTCHAR state_[SQL_SQLSTATE_SIZE+1]; - QVarLengthArray<SQLTCHAR> description_(SQL_MAX_MESSAGE_LENGTH); - QString result; - int i = 1; + QVarLengthArray<SQLTCHAR, SQL_SQLSTATE_SIZE + 1> state(SQL_SQLSTATE_SIZE + 1); + QVarLengthArray<SQLTCHAR, SQL_MAX_MESSAGE_LENGTH + 1> description(SQL_MAX_MESSAGE_LENGTH + 1); + QList<DiagRecord> result; - description_[0] = 0; + if (!handle) + return result; do { r = SQLGetDiagRec(handleType, handle, i, - state_, - &nativeCode_, - 0, - 0, + state.data(), + &nativeCode, + description.data(), + description.size(), &msgLen); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && msgLen > 0) - description_.resize(msgLen+1); - r = SQLGetDiagRec(handleType, - handle, - i, - state_, - &nativeCode_, - description_.data(), - description_.size(), - &msgLen); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (nativeCode) - *nativeCode = nativeCode_; - const QString tmpstore = fromSQLTCHAR(description_, msgLen); - if (result != tmpstore) { - if (!result.isEmpty()) - result += u' '; - result += tmpstore; - } + if (msgLen >= description.size()) { + description.resize(msgLen + 1); // incl. \0 termination + continue; + } + if (SQL_SUCCEEDED(r)) { + result.push_back({fromSQLTCHAR(description, msgLen), + fromSQLTCHAR(state), + QString::number(nativeCode)}); } else if (r == SQL_ERROR || r == SQL_INVALID_HANDLE) { - return result; + break; } ++i; } while (r != SQL_NO_DATA); return result; } -static QString qODBCWarn(const SQLHANDLE hStmt, const SQLHANDLE envHandle = 0, - const SQLHANDLE pDbC = 0, int *nativeCode = nullptr) +static QList<DiagRecord> qODBCWarn(const SQLHANDLE hStmt, + const SQLHANDLE envHandle = nullptr, + const SQLHANDLE pDbC = nullptr) { - QString result; - if (envHandle) - result += qWarnODBCHandle(SQL_HANDLE_ENV, envHandle, nativeCode); - if (pDbC) { - const QString dMessage = qWarnODBCHandle(SQL_HANDLE_DBC, pDbC, nativeCode); - if (!dMessage.isEmpty()) { - if (!result.isEmpty()) - result += u' '; - result += dMessage; - } - } - if (hStmt) { - const QString hMessage = qWarnODBCHandle(SQL_HANDLE_STMT, hStmt, nativeCode); - if (!hMessage.isEmpty()) { - if (!result.isEmpty()) - result += u' '; - result += hMessage; - } - } + QList<DiagRecord> result; + result.append(qWarnODBCHandle(SQL_HANDLE_ENV, envHandle)); + result.append(qWarnODBCHandle(SQL_HANDLE_DBC, pDbC)); + result.append(qWarnODBCHandle(SQL_HANDLE_STMT, hStmt)); return result; } -static QString qODBCWarn(const QODBCResultPrivate* odbc, int *nativeCode = nullptr) +static QList<DiagRecord> qODBCWarn(const QODBCResultPrivate *odbc) +{ + return qODBCWarn(odbc->hStmt, odbc->dpEnv(), odbc->dpDbc()); +} + +static QList<DiagRecord> qODBCWarn(const QODBCDriverPrivate *odbc) { - return qODBCWarn(odbc->hStmt, odbc->dpEnv(), odbc->dpDbc(), nativeCode); + return qODBCWarn(nullptr, odbc->hEnv, odbc->hDbc); } -static QString qODBCWarn(const QODBCDriverPrivate* odbc, int *nativeCode = nullptr) +static DiagRecord combineRecords(const QList<DiagRecord> &records) { - return qODBCWarn(0, odbc->hEnv, odbc->hDbc, nativeCode); + const auto add = [](const DiagRecord &a, const DiagRecord &b) { + return DiagRecord{a.description + u' ' + b.description, + a.sqlState + u';' + b.sqlState, + a.errorCode + u';' + b.errorCode}; + }; + if (records.isEmpty()) + return {}; + return std::accumulate(std::next(records.begin()), records.end(), records.front(), add); } -static void qSqlWarning(const QString& message, const QODBCResultPrivate* odbc) +static QSqlError errorFromDiagRecords(const QString &err, + QSqlError::ErrorType type, + const QList<DiagRecord> &records) { - qWarning() << message << "\tError:" << qODBCWarn(odbc); + if (records.empty()) + return QSqlError("QODBC: unknown error"_L1, {}, type, {}); + const auto combined = combineRecords(records); + return QSqlError("QODBC: "_L1 + err, combined.description + ", "_L1 + combined.sqlState, type, + combined.errorCode); } -static void qSqlWarning(const QString &message, const QODBCDriverPrivate *odbc) +static QString errorStringFromDiagRecords(const QList<DiagRecord>& records) { - qWarning() << message << "\tError:" << qODBCWarn(odbc); + const auto combined = combineRecords(records); + return combined.description; } -static void qSqlWarning(const QString &message, const SQLHANDLE hStmt) +template<class T> +static void qSqlWarning(const QString &message, T &&val) { - qWarning() << message << "\tError:" << qODBCWarn(hStmt); + const auto addMsg = errorStringFromDiagRecords(qODBCWarn(val)); + if (addMsg.isEmpty()) + qCWarning(lcOdbc) << message; + else + qCWarning(lcOdbc) << message << "\tError:" << addMsg; } -static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, const QODBCResultPrivate* p) +static QSqlError qMakeError(const QString &err, + QSqlError::ErrorType type, + const QODBCResultPrivate *p) { - int nativeCode = -1; - QString message = qODBCWarn(p, &nativeCode); - return QSqlError("QODBC: "_L1 + err, message, type, - nativeCode != -1 ? QString::number(nativeCode) : QString()); + return errorFromDiagRecords(err, type, qODBCWarn(p)); } -static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, - const QODBCDriverPrivate* p) +static QSqlError qMakeError(const QString &err, + QSqlError::ErrorType type, + const QODBCDriverPrivate *p) { - int nativeCode = -1; - QString message = qODBCWarn(p, &nativeCode); - return QSqlError("QODBC: "_L1 + err, message, type, - nativeCode != -1 ? QString::number(nativeCode) : QString()); + return errorFromDiagRecords(err, type, qODBCWarn(p)); } static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) @@ -348,6 +378,7 @@ static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) case SQL_TYPE_DATE: type = QMetaType::QDate; break; + case SQL_SS_TIME2: case SQL_TIME: case SQL_TYPE_TIME: type = QMetaType::QTime; @@ -376,115 +407,69 @@ static QMetaType qDecodeODBCType(SQLSMALLINT sqltype, bool isSigned = true) return QMetaType(type); } -static QVariant qGetStringData(SQLHANDLE hStmt, int column, int colSize, bool unicode) +template <typename CT> +static QVariant getStringDataImpl(SQLHANDLE hStmt, SQLUSMALLINT column, qsizetype colSize, SQLSMALLINT targetType) { QString fieldVal; SQLRETURN r = SQL_ERROR; SQLLEN lengthIndicator = 0; - - // NB! colSize must be a multiple of 2 for unicode enabled DBs - if (colSize <= 0) { - colSize = 256; - } else if (colSize > 65536) { // limit buffer size to 64 KB - colSize = 65536; - } else { - colSize++; // make sure there is room for more than the 0 termination - } - if (unicode) { + QVarLengthArray<CT> buf(colSize); + while (true) { r = SQLGetData(hStmt, - column+1, - SQL_C_TCHAR, - NULL, - 0, - &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0) - colSize = int(lengthIndicator / sizeof(SQLTCHAR) + 1); - QVarLengthArray<SQLTCHAR> buf(colSize); - memset(buf.data(), 0, colSize*sizeof(SQLTCHAR)); - while (true) { - r = SQLGetData(hStmt, - column+1, - SQL_C_TCHAR, - (SQLPOINTER)buf.data(), - colSize*sizeof(SQLTCHAR), - &lengthIndicator); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (lengthIndicator == SQL_NULL_DATA) { - return {}; - } - // starting with ODBC Native Client 2012, SQL_NO_TOTAL is returned - // instead of the length (which sometimes was wrong in older versions) - // see link for more info: http://msdn.microsoft.com/en-us/library/jj219209.aspx - // if length indicator equals SQL_NO_TOTAL, indicating that - // more data can be fetched, but size not known, collect data - // and fetch next block - if (lengthIndicator == SQL_NO_TOTAL) { - fieldVal += fromSQLTCHAR(buf, colSize); - continue; - } - // if SQL_SUCCESS_WITH_INFO is returned, indicating that - // more data can be fetched, the length indicator does NOT - // contain the number of bytes returned - it contains the - // total number of bytes that CAN be fetched - int rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : int(lengthIndicator / sizeof(SQLTCHAR)); - fieldVal += fromSQLTCHAR(buf, rSize); - if (lengthIndicator < SQLLEN(colSize*sizeof(SQLTCHAR))) { - // workaround for Drivermanagers that don't return SQL_NO_DATA - break; - } - } else if (r == SQL_NO_DATA) { - break; - } else { - qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')'; + column + 1, + targetType, + SQLPOINTER(buf.data()), SQLINTEGER(buf.size() * sizeof(CT)), + &lengthIndicator); + if (SQL_SUCCEEDED(r)) { + if (lengthIndicator == SQL_NULL_DATA) { return {}; } - } - } else { - r = SQLGetData(hStmt, - column+1, - SQL_C_CHAR, - NULL, - 0, - &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && lengthIndicator > 0) - colSize = lengthIndicator + 1; - QVarLengthArray<SQLCHAR> buf(colSize); - while (true) { - r = SQLGetData(hStmt, - column+1, - SQL_C_CHAR, - (SQLPOINTER)buf.data(), - colSize, - &lengthIndicator); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - if (lengthIndicator == SQL_NULL_DATA || lengthIndicator == SQL_NO_TOTAL) { - return {}; - } - // if SQL_SUCCESS_WITH_INFO is returned, indicating that - // more data can be fetched, the length indicator does NOT - // contain the number of bytes returned - it contains the - // total number of bytes that CAN be fetched - qsizetype rSize = (r == SQL_SUCCESS_WITH_INFO) ? colSize : lengthIndicator; - // Remove any trailing \0 as some drivers misguidedly append one - int realsize = qMin(rSize, buf.size()); - if (realsize > 0 && buf[realsize - 1] == 0) - realsize--; - fieldVal += QString::fromUtf8(reinterpret_cast<const char *>(buf.constData()), realsize); - if (lengthIndicator < SQLLEN(colSize)) { - // workaround for Drivermanagers that don't return SQL_NO_DATA - break; - } - } else if (r == SQL_NO_DATA) { + // starting with ODBC Native Client 2012, SQL_NO_TOTAL is returned + // instead of the length (which sometimes was wrong in older versions) + // see link for more info: http://msdn.microsoft.com/en-us/library/jj219209.aspx + // if length indicator equals SQL_NO_TOTAL, indicating that + // more data can be fetched, but size not known, collect data + // and fetch next block + if (lengthIndicator == SQL_NO_TOTAL) { + fieldVal += fromSQLTCHAR<QVarLengthArray<CT>, sizeof(CT)>(buf, buf.size()); + continue; + } + // if SQL_SUCCESS_WITH_INFO is returned, indicating that + // more data can be fetched, the length indicator does NOT + // contain the number of bytes returned - it contains the + // total number of bytes that CAN be fetched + const qsizetype rSize = (r == SQL_SUCCESS_WITH_INFO) + ? buf.size() + : qsizetype(lengthIndicator / sizeof(CT)); + fieldVal += fromSQLTCHAR<QVarLengthArray<CT>, sizeof(CT)>(buf, rSize); + // lengthIndicator does not contain the termination character + if (lengthIndicator < SQLLEN((buf.size() - 1) * sizeof(CT))) { + // workaround for Drivermanagers that don't return SQL_NO_DATA break; - } else { - qWarning() << "qGetStringData: Error while fetching data (" << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ')'; - return {}; } + } else if (r == SQL_NO_DATA) { + break; + } else { + qSqlWarning("QODBC::getStringData: Error while fetching data"_L1, hStmt); + return {}; } } return fieldVal; } +static QVariant qGetStringData(SQLHANDLE hStmt, SQLUSMALLINT column, int colSize, bool unicode) +{ + if (colSize <= 0) { + colSize = 256; // default Prealloc size of QVarLengthArray + } else if (colSize > 65536) { // limit buffer size to 64 KB + colSize = 65536; + } else { + colSize++; // make sure there is room for more than the 0 termination + } + return unicode ? getStringDataImpl<SQLTCHAR>(hStmt, column, colSize, SQL_C_TCHAR) + : getStringDataImpl<SQLCHAR>(hStmt, column, colSize, SQL_C_CHAR); +} + static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) { QByteArray fieldVal; @@ -496,19 +481,19 @@ static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) SQLLEN lengthIndicator = 0; SQLRETURN r = SQL_ERROR; - QVarLengthArray<SQLTCHAR> colName(COLNAMESIZE); + QVarLengthArray<SQLTCHAR, COLNAMESIZE> colName(COLNAMESIZE); r = SQLDescribeCol(hStmt, column + 1, - colName.data(), - COLNAMESIZE, + colName.data(), SQLSMALLINT(colName.size()), &colNameLen, &colType, &colSize, &colScale, &nullable); if (r != SQL_SUCCESS) - qWarning() << "qGetBinaryData: Unable to describe column" << column; + qSqlWarning(("QODBC::qGetBinaryData: Unable to describe column %1"_L1) + .arg(QString::number(column)), hStmt); // SQLDescribeCol may return 0 if size cannot be determined if (!colSize) colSize = 255; @@ -523,7 +508,7 @@ static QVariant qGetBinaryData(SQLHANDLE hStmt, int column) const_cast<char *>(fieldVal.constData() + read), colSize, &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) break; if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType(QMetaType::QByteArray)); @@ -552,7 +537,7 @@ static QVariant qGetIntData(SQLHANDLE hStmt, int column, bool isSigned = true) (SQLPOINTER)&intbuf, sizeof(intbuf), &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) return QVariant(); if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType::fromType<int>()); @@ -572,7 +557,7 @@ static QVariant qGetDoubleData(SQLHANDLE hStmt, int column) (SQLPOINTER) &dblbuf, 0, &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { return QVariant(); } if (lengthIndicator == SQL_NULL_DATA) @@ -592,7 +577,7 @@ static QVariant qGetBigIntData(SQLHANDLE hStmt, int column, bool isSigned = true (SQLPOINTER) &lngbuf, sizeof(lngbuf), &lengthIndicator); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) + if (!SQL_SUCCEEDED(r)) return QVariant(); if (lengthIndicator == SQL_NULL_DATA) return QVariant(QMetaType::fromType<qlonglong>()); @@ -608,16 +593,14 @@ static bool isAutoValue(const SQLHANDLE hStmt, int column) SQLLEN nNumericAttribute = 0; // Check for auto-increment const SQLRETURN r = ::SQLColAttribute(hStmt, column + 1, SQL_DESC_AUTO_UNIQUE_VALUE, 0, 0, 0, &nNumericAttribute); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { - qSqlWarning(QStringLiteral("qMakeField: Unable to get autovalue attribute for column ") - + QString::number(column), hStmt); + if (!SQL_SUCCEEDED(r)) { + qSqlWarning(("QODBC::isAutoValue: Unable to get autovalue attribute for column %1"_L1) + .arg(QString::number(column)), hStmt); return false; } return nNumericAttribute != SQL_FALSE; } -static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMessage); - // creates a QSqlField from a valid hStmt generated // by SQLColumns. The hStmt has to point to a valid position. static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* p) @@ -629,7 +612,6 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* f.setLength(var.isNull() ? -1 : var.toInt()); // column size var = qGetIntData(hStmt, 8).toInt(); f.setPrecision(var.isNull() ? -1 : var.toInt()); // precision - f.setSqlType(type); int required = qGetIntData(hStmt, 10).toInt(); // nullable-flag // required can be SQL_NO_NULLS, SQL_NULLABLE or SQL_NULLABLE_UNKNOWN if (required == SQL_NO_NULLS) @@ -640,16 +622,7 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, const QODBCDriverPrivate* return f; } -static QSqlField qMakeFieldInfo(const QODBCResultPrivate* p, int i ) -{ - QString errorMessage; - const QSqlField result = qMakeFieldInfo(p->hStmt, i, &errorMessage); - if (!errorMessage.isEmpty()) - qSqlWarning(errorMessage, p); - return result; -} - -static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMessage) +static QSqlField qMakeFieldInfo(const QODBCResultPrivate *p, int i) { SQLSMALLINT colNameLen; SQLSMALLINT colType; @@ -657,12 +630,10 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess SQLSMALLINT colScale; SQLSMALLINT nullable; SQLRETURN r = SQL_ERROR; - QVarLengthArray<SQLTCHAR> colName(COLNAMESIZE); - errorMessage->clear(); - r = SQLDescribeCol(hStmt, + QVarLengthArray<SQLTCHAR, COLNAMESIZE> colName(COLNAMESIZE); + r = SQLDescribeCol(p->hStmt, i+1, - colName.data(), - (SQLSMALLINT)COLNAMESIZE, + colName.data(), SQLSMALLINT(colName.size()), &colNameLen, &colType, &colSize, @@ -670,12 +641,13 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess &nullable); if (r != SQL_SUCCESS) { - *errorMessage = QStringLiteral("qMakeField: Unable to describe column ") + QString::number(i); + qSqlWarning(("QODBC::qMakeFieldInfo: Unable to describe column %1"_L1) + .arg(QString::number(i)), p); return QSqlField(); } SQLLEN unsignedFlag = SQL_FALSE; - r = SQLColAttribute (hStmt, + r = SQLColAttribute (p->hStmt, i + 1, SQL_DESC_UNSIGNED, 0, @@ -683,15 +655,14 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess 0, &unsignedFlag); if (r != SQL_SUCCESS) { - qSqlWarning(QStringLiteral("qMakeField: Unable to get column attributes for column ") - + QString::number(i), hStmt); + qSqlWarning(("QODBC::qMakeFieldInfo: Unable to get column attributes for column %1"_L1) + .arg(QString::number(i)), p); } const QString qColName(fromSQLTCHAR(colName, colNameLen)); // nullable can be SQL_NO_NULLS, SQL_NULLABLE or SQL_NULLABLE_UNKNOWN QMetaType type = qDecodeODBCType(colType, unsignedFlag == SQL_FALSE); QSqlField f(qColName, type); - f.setSqlType(colType); f.setLength(colSize == 0 ? -1 : int(colSize)); f.setPrecision(colScale == 0 ? -1 : int(colScale)); if (nullable == SQL_NO_NULLS) @@ -699,10 +670,10 @@ static QSqlField qMakeFieldInfo(const SQLHANDLE hStmt, int i, QString *errorMess else if (nullable == SQL_NULLABLE) f.setRequired(false); // else we don't know - f.setAutoValue(isAutoValue(hStmt, i)); - QVarLengthArray<SQLTCHAR> tableName(TABLENAMESIZE); + f.setAutoValue(isAutoValue(p->hStmt, i)); + QVarLengthArray<SQLTCHAR, TABLENAMESIZE> tableName(TABLENAMESIZE); SQLSMALLINT tableNameLen; - r = SQLColAttribute(hStmt, + r = SQLColAttribute(p->hStmt, i + 1, SQL_DESC_BASE_TABLE_NAME, tableName.data(), @@ -731,7 +702,7 @@ QChar QODBCDriverPrivate::quoteChar() &driverResponse, sizeof(driverResponse), &length); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) quote = QChar(driverResponse[0]); else quote = u'"'; @@ -740,7 +711,19 @@ QChar QODBCDriverPrivate::quoteChar() return quote; } -static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, const QString &val) +SQLRETURN QODBCDriverPrivate::sqlFetchNext(const SqlStmtHandle &hStmt) const +{ + return sqlFetchNext(hStmt.handle()); +} + +SQLRETURN QODBCDriverPrivate::sqlFetchNext(SQLHANDLE hStmt) const +{ + if (hasSQLFetchScroll) + return SQLFetchScroll(hStmt, SQL_FETCH_NEXT, 0); + return SQLFetch(hStmt); +} + +static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, QStringView val) { auto encoded = toSQLTCHAR(val); return SQLSetConnectAttr(handle, attr, @@ -749,101 +732,106 @@ static SQLRETURN qt_string_SQLSetConnectAttr(SQLHDBC handle, SQLINTEGER attr, co } -bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) +bool QODBCDriverPrivate::setConnectionOptions(const QString &connOpts) { // Set any connection attributes - const QStringList opts(connOpts.split(u';', Qt::SkipEmptyParts)); SQLRETURN r = SQL_SUCCESS; - for (int i = 0; i < opts.count(); ++i) { - const QString tmp(opts.at(i)); + for (const auto connOpt : QStringTokenizer{connOpts, u';'}) { int idx; - if ((idx = tmp.indexOf(u'=')) == -1) { - qWarning() << "QODBCDriver::open: Illegal connect option value '" << tmp << '\''; + if ((idx = connOpt.indexOf(u'=')) == -1) { + qSqlWarning(("QODBCDriver::open: Illegal connect option value '%1'"_L1) + .arg(connOpt), this); continue; } - const QString opt(tmp.left(idx)); - const QString val(tmp.mid(idx + 1).simplified()); + const auto opt(connOpt.left(idx)); + const auto val(connOpt.mid(idx + 1).trimmed()); SQLUINTEGER v = 0; r = SQL_SUCCESS; - if (opt.toUpper() == "SQL_ATTR_ACCESS_MODE"_L1) { - if (val.toUpper() == "SQL_MODE_READ_ONLY"_L1) { + if (opt == "SQL_ATTR_ACCESS_MODE"_L1) { + if (val == "SQL_MODE_READ_ONLY"_L1) { v = SQL_MODE_READ_ONLY; - } else if (val.toUpper() == "SQL_MODE_READ_WRITE"_L1) { + } else if (val == "SQL_MODE_READ_WRITE"_L1) { v = SQL_MODE_READ_WRITE; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_ACCESS_MODE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CONNECTION_TIMEOUT"_L1) { + } else if (opt == "SQL_ATTR_CONNECTION_TIMEOUT"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_CONNECTION_TIMEOUT, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_LOGIN_TIMEOUT"_L1) { + } else if (opt == "SQL_ATTR_LOGIN_TIMEOUT"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_LOGIN_TIMEOUT, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CURRENT_CATALOG"_L1) { + } else if (opt == "SQL_ATTR_CURRENT_CATALOG"_L1) { r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_CURRENT_CATALOG, val); - } else if (opt.toUpper() == "SQL_ATTR_METADATA_ID"_L1) { - if (val.toUpper() == "SQL_TRUE"_L1) { + } else if (opt == "SQL_ATTR_METADATA_ID"_L1) { + if (val == "SQL_TRUE"_L1) { v = SQL_TRUE; - } else if (val.toUpper() == "SQL_FALSE"_L1) { + } else if (val == "SQL_FALSE"_L1) { v = SQL_FALSE; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_METADATA_ID, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_PACKET_SIZE"_L1) { + } else if (opt == "SQL_ATTR_PACKET_SIZE"_L1) { v = val.toUInt(); r = SQLSetConnectAttr(hDbc, SQL_ATTR_PACKET_SIZE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_TRACEFILE"_L1) { + } else if (opt == "SQL_ATTR_TRACEFILE"_L1) { r = qt_string_SQLSetConnectAttr(hDbc, SQL_ATTR_TRACEFILE, val); - } else if (opt.toUpper() == "SQL_ATTR_TRACE"_L1) { - if (val.toUpper() == "SQL_OPT_TRACE_OFF"_L1) { + } else if (opt == "SQL_ATTR_TRACE"_L1) { + if (val == "SQL_OPT_TRACE_OFF"_L1) { v = SQL_OPT_TRACE_OFF; - } else if (val.toUpper() == "SQL_OPT_TRACE_ON"_L1) { + } else if (val == "SQL_OPT_TRACE_ON"_L1) { v = SQL_OPT_TRACE_ON; } else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_TRACE, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CONNECTION_POOLING"_L1) { + } else if (opt == "SQL_ATTR_CONNECTION_POOLING"_L1) { if (val == "SQL_CP_OFF"_L1) v = SQL_CP_OFF; - else if (val.toUpper() == "SQL_CP_ONE_PER_DRIVER"_L1) + else if (val == "SQL_CP_ONE_PER_DRIVER"_L1) v = SQL_CP_ONE_PER_DRIVER; - else if (val.toUpper() == "SQL_CP_ONE_PER_HENV"_L1) + else if (val == "SQL_CP_ONE_PER_HENV"_L1) v = SQL_CP_ONE_PER_HENV; - else if (val.toUpper() == "SQL_CP_DEFAULT"_L1) + else if (val == "SQL_CP_DEFAULT"_L1) v = SQL_CP_DEFAULT; else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_CONNECTION_POOLING, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_CP_MATCH"_L1) { - if (val.toUpper() == "SQL_CP_STRICT_MATCH"_L1) + } else if (opt == "SQL_ATTR_CP_MATCH"_L1) { + if (val == "SQL_CP_STRICT_MATCH"_L1) v = SQL_CP_STRICT_MATCH; - else if (val.toUpper() == "SQL_CP_RELAXED_MATCH"_L1) + else if (val == "SQL_CP_RELAXED_MATCH"_L1) v = SQL_CP_RELAXED_MATCH; - else if (val.toUpper() == "SQL_CP_MATCH_DEFAULT"_L1) + else if (val == "SQL_CP_MATCH_DEFAULT"_L1) v = SQL_CP_MATCH_DEFAULT; else { - qWarning() << "QODBCDriver::open: Unknown option value '" << val << '\''; + qSqlWarning(("QODBCDriver::open: Unknown option value '%1'"_L1) + .arg(val), this); continue; } r = SQLSetConnectAttr(hDbc, SQL_ATTR_CP_MATCH, (SQLPOINTER) size_t(v), 0); - } else if (opt.toUpper() == "SQL_ATTR_ODBC_VERSION"_L1) { + } else if (opt == "SQL_ATTR_ODBC_VERSION"_L1) { // Already handled in QODBCDriver::open() continue; } else { - qWarning() << "QODBCDriver::open: Unknown connection attribute '" << opt << '\''; + qSqlWarning(("QODBCDriver::open: Unknown connection attribute '%1'"_L1) + .arg(opt), this); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) - qSqlWarning(QString::fromLatin1("QODBCDriver::open: Unable to set connection attribute'%1'").arg( - opt), this); + if (!SQL_SUCCEEDED(r)) + qSqlWarning(("QODBCDriver::open: Unable to set connection attribute '%1'"_L1) + .arg(opt), this); } return true; } @@ -851,60 +839,65 @@ bool QODBCDriverPrivate::setConnectionOptions(const QString& connOpts) void QODBCDriverPrivate::splitTableQualifier(const QString &qualifier, QString &catalog, QString &schema, QString &table) const { + Q_Q(const QODBCDriver); + const auto adjustName = [&](const QString &name) { + if (q->isIdentifierEscaped(name, QSqlDriver::TableName)) + return q->stripDelimiters(name, QSqlDriver::TableName); + return adjustCase(name); + }; + catalog.clear(); + schema.clear(); + table.clear(); if (!useSchema) { - table = qualifier; + table = adjustName(qualifier); return; } const QList<QStringView> l = QStringView(qualifier).split(u'.'); switch (l.count()) { case 1: - table = qualifier; + table = adjustName(qualifier); break; case 2: - schema = l.at(0).toString(); - table = l.at(1).toString(); + schema = adjustName(l.at(0).toString()); + table = adjustName(l.at(1).toString()); break; case 3: - catalog = l.at(0).toString(); - schema = l.at(1).toString(); - table = l.at(2).toString(); + catalog = adjustName(l.at(0).toString()); + schema = adjustName(l.at(1).toString()); + table = adjustName(l.at(2).toString()); break; default: - qSqlWarning(QString::fromLatin1("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'") + qSqlWarning(("QODBCDriver::splitTableQualifier: Unable to split table qualifier '%1'"_L1) .arg(qualifier), this); break; } } -QODBCDriverPrivate::DefaultCase QODBCDriverPrivate::defaultCase() const +void QODBCDriverPrivate::checkDefaultCase() { - DefaultCase ret; + m_defaultCase = DefaultCase::Mixed; //arbitrary case if driver cannot be queried SQLUSMALLINT casing; - int r = SQLGetInfo(hDbc, - SQL_IDENTIFIER_CASE, - &casing, - sizeof(casing), - NULL); - if ( r != SQL_SUCCESS) - ret = Mixed;//arbitrary case if driver cannot be queried - else { + SQLRETURN r = SQLGetInfo(hDbc, + SQL_IDENTIFIER_CASE, + &casing, + sizeof(casing), + NULL); + if (r == SQL_SUCCESS) { switch (casing) { - case (SQL_IC_UPPER): - ret = Upper; - break; - case (SQL_IC_LOWER): - ret = Lower; - break; - case (SQL_IC_SENSITIVE): - ret = Sensitive; - break; - case (SQL_IC_MIXED): - default: - ret = Mixed; - break; + case SQL_IC_UPPER: + m_defaultCase = DefaultCase::Upper; + break; + case SQL_IC_LOWER: + m_defaultCase = DefaultCase::Lower; + break; + case SQL_IC_SENSITIVE: + m_defaultCase = DefaultCase::Sensitive; + break; + case SQL_IC_MIXED: + m_defaultCase = DefaultCase::Mixed; + break; } } - return ret; } /* @@ -913,20 +906,16 @@ QODBCDriverPrivate::DefaultCase QODBCDriverPrivate::defaultCase() const */ QString QODBCDriverPrivate::adjustCase(const QString &identifier) const { - QString ret = identifier; - switch(defaultCase()) { - case (Lower): - ret = identifier.toLower(); - break; - case (Upper): - ret = identifier.toUpper(); - break; - case(Mixed): - case(Sensitive): - default: - ret = identifier; + switch (m_defaultCase) { + case DefaultCase::Lower: + return identifier.toLower(); + case DefaultCase::Upper: + return identifier.toUpper(); + case DefaultCase::Mixed: + case DefaultCase::Sensitive: + break; } - return ret; + return identifier; } //////////////////////////////////////////////////////////////////////////// @@ -942,8 +931,7 @@ QODBCResult::~QODBCResult() if (d->hStmt && d->isStmtHandleValid() && driver() && driver()->isOpen()) { SQLRETURN r = SQLFreeHandle(SQL_HANDLE_STMT, d->hStmt); if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle "_L1 - + QString::number(r), d); + qSqlWarning(("QODBCResult: Unable to free statement handle "_L1), d); } } @@ -987,7 +975,7 @@ bool QODBCResult::reset (const QString& query) (SQLPOINTER)SQL_CURSOR_STATIC, SQL_IS_UINTEGER); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "QODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. " "Please check your ODBC driver configuration"), QSqlError::StatementError, d)); @@ -1000,7 +988,7 @@ bool QODBCResult::reset (const QString& query) encoded.data(), SQLINTEGER(encoded.size())); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r!= SQL_NO_DATA) { + if (!SQL_SUCCEEDED(r) && r!= SQL_NO_DATA) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); return false; @@ -1008,7 +996,7 @@ bool QODBCResult::reset (const QString& query) SQLULEN isScrollable = 0; r = SQLGetStmtAttr(d->hStmt, SQL_ATTR_CURSOR_SCROLLABLE, &isScrollable, SQL_IS_INTEGER, 0); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) setForwardOnly(isScrollable == SQL_NONSCROLLABLE); SQLSMALLINT count = 0; @@ -1077,7 +1065,7 @@ bool QODBCResult::fetchNext() else r = SQLFetch(d->hStmt); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { if (r != SQL_NO_DATA) setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to fetch next"), QSqlError::ConnectionError, d)); @@ -1174,7 +1162,8 @@ QVariant QODBCResult::data(int field) { Q_D(QODBCResult); if (field >= d->rInf.count() || field < 0) { - qWarning() << "QODBCResult::data: column" << field << "out of range"; + qSqlWarning(("QODBCResult::data: column %1 out of range"_L1) + .arg(QString::number(field)), d); return QVariant(); } if (field < d->fieldCacheIdx) @@ -1210,7 +1199,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&dbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QDate(dbuf.year, dbuf.month, dbuf.day)); else d->fieldCache[i] = QVariant(QMetaType::fromType<QDate>()); @@ -1223,7 +1212,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&tbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QTime(tbuf.hour, tbuf.minute, tbuf.second)); else d->fieldCache[i] = QVariant(QMetaType::fromType<QTime>()); @@ -1236,7 +1225,7 @@ QVariant QODBCResult::data(int field) (SQLPOINTER)&dtbuf, 0, &lengthIndicator); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (lengthIndicator != SQL_NULL_DATA)) + if (SQL_SUCCEEDED(r) && (lengthIndicator != SQL_NULL_DATA)) d->fieldCache[i] = QVariant(QDateTime(QDate(dtbuf.year, dtbuf.month, dtbuf.day), QTime(dtbuf.hour, dtbuf.minute, dtbuf.second, dtbuf.fraction / 1000000))); else @@ -1300,8 +1289,7 @@ int QODBCResult::numRowsAffected() SQLRETURN r = SQLRowCount(d->hStmt, &affectedRowCount); if (r == SQL_SUCCESS) return affectedRowCount; - else - qSqlWarning("QODBCResult::numRowsAffected: Unable to count affected rows"_L1, d); + qSqlWarning("QODBCResult::numRowsAffected: Unable to count affected rows"_L1, d); return -1; } @@ -1341,7 +1329,7 @@ bool QODBCResult::prepare(const QString& query) (SQLPOINTER)SQL_CURSOR_STATIC, SQL_IS_UINTEGER); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "QODBCResult::reset: Unable to set 'SQL_CURSOR_STATIC' as statement attribute. " "Please check your ODBC driver configuration"), QSqlError::StatementError, d)); @@ -1382,8 +1370,7 @@ bool QODBCResult::exec() QVariantList &values = boundValues(); QByteArrayList tmpStorage(values.count(), QByteArray()); // targets for SQLBindParameter() - QVarLengthArray<SQLLEN, 32> indicators(values.count()); - memset(indicators.data(), 0, indicators.size() * sizeof(SQLLEN)); + QVarLengthArray<SQLLEN, 32> indicators(values.count(), 0); // bind parameters - only positional binding allowed SQLRETURN r; @@ -1667,15 +1654,15 @@ bool QODBCResult::exec() break; } } if (r != SQL_SUCCESS) { - qWarning() << "QODBCResult::exec: unable to bind variable:" << qODBCWarn(d); + qSqlWarning("QODBCResult::exec: unable to bind variable:"_L1, d); setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to bind variable"), QSqlError::StatementError, d)); return false; } } r = SQLExecute(d->hStmt); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { - qWarning() << "QODBCResult::exec: Unable to execute statement:" << qODBCWarn(d); + if (!SQL_SUCCEEDED(r) && r != SQL_NO_DATA) { + qSqlWarning("QODBCResult::exec: Unable to execute statement:"_L1, d); setLastError(qMakeError(QCoreApplication::translate("QODBCResult", "Unable to execute statement"), QSqlError::StatementError, d)); return false; @@ -1683,7 +1670,7 @@ bool QODBCResult::exec() SQLULEN isScrollable = 0; r = SQLGetStmtAttr(d->hStmt, SQL_ATTR_CURSOR_SCROLLABLE, &isScrollable, SQL_IS_INTEGER, 0); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) setForwardOnly(isScrollable == SQL_NONSCROLLABLE); SQLSMALLINT count = 0; @@ -1815,9 +1802,7 @@ bool QODBCResult::nextResult() SQLRETURN r = SQLMoreResults(d->hStmt); if (r != SQL_SUCCESS) { if (r == SQL_SUCCESS_WITH_INFO) { - int nativeCode = -1; - QString message = qODBCWarn(d, &nativeCode); - qWarning() << "QODBCResult::nextResult():" << message; + qSqlWarning("QODBCResult::nextResult:"_L1, d); } else { if (r != SQL_NO_DATA) setLastError(qMakeError(QCoreApplication::translate("QODBCResult", @@ -1936,6 +1921,18 @@ bool QODBCDriver::open(const QString & db, int, const QString& connOpts) { + const auto ensureEscaped = [](QString arg) -> QString { + QChar quoteChar; + if (arg.startsWith(u'"')) + quoteChar = u'\''; + else if (arg.startsWith(u'\'')) + quoteChar = u'"'; + else if (arg.contains(u';')) + quoteChar = u'"'; + else + return arg; + return quoteChar + arg + quoteChar; + }; Q_D(QODBCDriver); if (isOpen()) close(); @@ -1943,7 +1940,7 @@ bool QODBCDriver::open(const QString & db, r = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &d->hEnv); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { qSqlWarning("QODBCDriver::open: Unable to allocate environment"_L1, d); setOpenError(true); return false; @@ -1955,7 +1952,7 @@ bool QODBCDriver::open(const QString & db, r = SQLAllocHandle(SQL_HANDLE_DBC, d->hEnv, &d->hDbc); - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { qSqlWarning("QODBCDriver::open: Unable to allocate connection"_L1, d); setOpenError(true); cleanup(); @@ -1971,33 +1968,31 @@ bool QODBCDriver::open(const QString & db, QString connQStr; // support the "DRIVER={SQL SERVER};SERVER=blah" syntax if (db.contains(".dsn"_L1, Qt::CaseInsensitive)) - connQStr = "FILEDSN="_L1 + db; + connQStr = "FILEDSN="_L1 + ensureEscaped(db); else if (db.contains("DRIVER="_L1, Qt::CaseInsensitive) || db.contains("SERVER="_L1, Qt::CaseInsensitive)) connQStr = db; else - connQStr = "DSN="_L1 + db; + connQStr = "DSN="_L1 + ensureEscaped(db); if (!user.isEmpty()) - connQStr += ";UID="_L1 + user; + connQStr += ";UID="_L1 + ensureEscaped(user); if (!password.isEmpty()) - connQStr += ";PWD="_L1 + password; + connQStr += ";PWD="_L1 + ensureEscaped(password); SQLSMALLINT cb; - QVarLengthArray<SQLTCHAR> connOut(1024); - memset(connOut.data(), 0, connOut.size() * sizeof(SQLTCHAR)); + QVarLengthArray<SQLTCHAR, 1024> connOut(1024); { auto encoded = toSQLTCHAR(connQStr); r = SQLDriverConnect(d->hDbc, nullptr, encoded.data(), SQLSMALLINT(encoded.size()), - connOut.data(), - 1024, + connOut.data(), SQLSMALLINT(connOut.size()), &cb, /*SQL_DRIVER_NOPROMPT*/0); } - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) { + if (!SQL_SUCCEEDED(r)) { setLastError(qMakeError(tr("Unable to connect"), QSqlError::ConnectionError, d)); setOpenError(true); cleanup(); @@ -2018,6 +2013,7 @@ bool QODBCDriver::open(const QString & db, d->checkHasSQLFetchScroll(); d->checkHasMultiResults(); d->checkDateTimePrecision(); + d->checkDefaultCase(); setOpen(true); setOpenError(false); if (d->dbmsType == MSSqlServer) { @@ -2076,7 +2072,7 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WCHAR)) { unicode = true; return; } @@ -2086,7 +2082,7 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WVARCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WVARCHAR)) { unicode = true; return; } @@ -2096,27 +2092,36 @@ void QODBCDriverPrivate::checkUnicode() (SQLPOINTER)&fFunc, sizeof(fFunc), NULL); - if ((r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) && (fFunc & SQL_CVT_WLONGVARCHAR)) { + if (SQL_SUCCEEDED(r) && (fFunc & SQL_CVT_WLONGVARCHAR)) { unicode = true; return; } - SQLHANDLE hStmt; - r = SQLAllocHandle(SQL_HANDLE_STMT, - hDbc, - &hStmt); - r = SQLExecDirect(hStmt, toSQLTCHAR("select 'test'"_L1).data(), SQL_NTS); + SqlStmtHandle hStmt(hDbc); + // for databases which do not return something useful in SQLGetInfo and are picky about a + // 'SELECT' statement without 'FROM' but support VALUE(foo) statement like e.g. DB2 or Oracle + const std::array<QStringView, 3> statements = { + u"select 'test'", + u"values('test')", + u"select 'test' from dual", + }; + for (const auto &statement : statements) { + auto encoded = toSQLTCHAR(statement); + r = SQLExecDirect(hStmt.handle(), encoded.data(), SQLINTEGER(encoded.size())); + if (r == SQL_SUCCESS) + break; + } if (r == SQL_SUCCESS) { - r = SQLFetch(hStmt); + r = SQLFetch(hStmt.handle()); if (r == SQL_SUCCESS) { - QVarLengthArray<SQLWCHAR> buffer(10); - r = SQLGetData(hStmt, 1, SQL_C_WCHAR, buffer.data(), buffer.size() * sizeof(SQLWCHAR), NULL); + QVarLengthArray<SQLWCHAR, 10> buffer(10); + r = SQLGetData(hStmt.handle(), 1, SQL_C_WCHAR, buffer.data(), + buffer.size() * sizeof(SQLWCHAR), NULL); if (r == SQL_SUCCESS && fromSQLTCHAR(buffer) == "test"_L1) { unicode = true; } } } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); } bool QODBCDriverPrivate::checkDriver() const @@ -2146,9 +2151,10 @@ bool QODBCDriverPrivate::checkDriver() const return false; } if (sup == SQL_FALSE) { - qWarning () << "QODBCDriver::open: Warning - Driver doesn't support all needed functionality (" - << func - << ").\nPlease look at the Qt SQL Module Driver documentation for more information."; + qSqlWarning(("QODBCDriver::checkDriver: Driver doesn't support all needed " + "functionality (func id %1).\nPlease look at the Qt SQL Module " + "Driver documentation for more information."_L1) + .arg(QString::number(func)), this); return false; } } @@ -2163,8 +2169,9 @@ bool QODBCDriverPrivate::checkDriver() const return false; } if (sup == SQL_FALSE) { - qWarning() << "QODBCDriver::checkDriver: Warning - Driver doesn't support some non-critical functions (" - << func << ')'; + qSqlWarning(("QODBCDriver::checkDriver: Driver doesn't support some " + "non-critical functions (func id %1)."_L1) + .arg(QString::number(func)), this); return true; } } @@ -2183,23 +2190,22 @@ void QODBCDriverPrivate::checkSchemaUsage() (SQLPOINTER) &val, sizeof(val), NULL); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) + if (SQL_SUCCEEDED(r)) useSchema = (val != 0); } void QODBCDriverPrivate::checkDBMS() { SQLRETURN r; - QVarLengthArray<SQLTCHAR> serverString(200); + QVarLengthArray<SQLTCHAR, 200> serverString(200); SQLSMALLINT t; - memset(serverString.data(), 0, serverString.size() * sizeof(SQLTCHAR)); r = SQLGetInfo(hDbc, SQL_DBMS_NAME, serverString.data(), SQLSMALLINT(serverString.size() * sizeof(SQLTCHAR)), &t); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { const QString serverType = fromSQLTCHAR(serverString, t / sizeof(SQLTCHAR)); if (serverType.contains("PostgreSQL"_L1, Qt::CaseInsensitive)) dbmsType = QSqlDriver::PostgreSQL; @@ -2217,7 +2223,7 @@ void QODBCDriverPrivate::checkDBMS() serverString.data(), SQLSMALLINT(serverString.size() * sizeof(SQLTCHAR)), &t); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { + if (SQL_SUCCEEDED(r)) { const QString serverType = fromSQLTCHAR(serverString, t / sizeof(SQLTCHAR)); isFreeTDSDriver = serverType.contains("tdsodbc"_L1, Qt::CaseInsensitive); unicode = unicode && !isFreeTDSDriver; @@ -2228,46 +2234,42 @@ void QODBCDriverPrivate::checkHasSQLFetchScroll() { SQLUSMALLINT sup; SQLRETURN r = SQLGetFunctions(hDbc, SQL_API_SQLFETCHSCROLL, &sup); - if ((r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO) || sup != SQL_TRUE) { + if ((!SQL_SUCCEEDED(r)) || sup != SQL_TRUE) { hasSQLFetchScroll = false; - qWarning("QODBCDriver::checkHasSQLFetchScroll: Warning - Driver doesn't support scrollable result sets, use forward only mode for queries"); + qSqlWarning("QODBCDriver::checkHasSQLFetchScroll: Driver doesn't support " + "scrollable result sets, use forward only mode for queries"_L1, this); } } void QODBCDriverPrivate::checkHasMultiResults() { - QVarLengthArray<SQLTCHAR> driverResponse(2); + QVarLengthArray<SQLTCHAR, 2> driverResponse(2); SQLSMALLINT length; SQLRETURN r = SQLGetInfo(hDbc, SQL_MULT_RESULT_SETS, driverResponse.data(), SQLSMALLINT(driverResponse.size() * sizeof(SQLTCHAR)), &length); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) - hasMultiResultSets = fromSQLTCHAR(driverResponse, length/sizeof(SQLTCHAR)).startsWith(u'Y'); + if (SQL_SUCCEEDED(r)) + hasMultiResultSets = fromSQLTCHAR(driverResponse, length / sizeof(SQLTCHAR)).startsWith(u'Y'); } void QODBCDriverPrivate::checkDateTimePrecision() { SQLINTEGER columnSize; - SQLHANDLE hStmt; + SqlStmtHandle hStmt(hDbc); - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt); - if (r != SQL_SUCCESS) { + if (!hStmt.isValid()) return; - } - r = SQLGetTypeInfo(hStmt, SQL_TIMESTAMP); - if (r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO) { - r = SQLFetch(hStmt); - if ( r == SQL_SUCCESS || r == SQL_SUCCESS_WITH_INFO ) - { - if (SQLGetData(hStmt, 3, SQL_INTEGER, &columnSize, sizeof(columnSize), 0) == SQL_SUCCESS) { + SQLRETURN r = SQLGetTypeInfo(hStmt.handle(), SQL_TIMESTAMP); + if (SQL_SUCCEEDED(r)) { + r = SQLFetch(hStmt.handle()); + if (SQL_SUCCEEDED(r)) { + if (SQLGetData(hStmt.handle(), 3, SQL_INTEGER, &columnSize, sizeof(columnSize), 0) == SQL_SUCCESS) datetimePrecision = (int)columnSize; - } } } - SQLFreeHandle(SQL_HANDLE_STMT, hStmt); } QSqlResult *QODBCDriver::createResult() const @@ -2279,7 +2281,7 @@ bool QODBCDriver::beginTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::beginTransaction: Database not open"); + qSqlWarning("QODBCDriver::beginTransaction: Database not open"_L1, d); return false; } SQLUINTEGER ac(SQL_AUTOCOMMIT_OFF); @@ -2299,7 +2301,7 @@ bool QODBCDriver::commitTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::commitTransaction: Database not open"); + qSqlWarning("QODBCDriver::commitTransaction: Database not open"_L1, d); return false; } SQLRETURN r = SQLEndTran(SQL_HANDLE_DBC, @@ -2317,7 +2319,7 @@ bool QODBCDriver::rollbackTransaction() { Q_D(QODBCDriver); if (!isOpen()) { - qWarning("QODBCDriver::rollbackTransaction: Database not open"); + qSqlWarning("QODBCDriver::rollbackTransaction: Database not open"_L1, d); return false; } SQLRETURN r = SQLEndTran(SQL_HANDLE_DBC, @@ -2352,19 +2354,16 @@ QStringList QODBCDriver::tables(QSql::TableType type) const QStringList tl; if (!isOpen()) return tl; - SQLHANDLE hStmt; - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { qSqlWarning("QODBCDriver::tables: Unable to allocate handle"_L1, d); return tl; } - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); QStringList tableType; if (type & QSql::Tables) tableType += "TABLE"_L1; @@ -2378,7 +2377,7 @@ QStringList QODBCDriver::tables(QSql::TableType type) const { auto joinedTableTypeString = toSQLTCHAR(tableType.join(u',')); - r = SQLTables(hStmt, + r = SQLTables(hStmt.handle(), nullptr, 0, nullptr, 0, nullptr, 0, @@ -2386,34 +2385,21 @@ QStringList QODBCDriver::tables(QSql::TableType type) const } if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, d); + qSqlWarning("QODBCDriver::tables Unable to execute table list"_L1, + hStmt.handle()); - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); - - if (r != SQL_SUCCESS && r != SQL_SUCCESS_WITH_INFO && r != SQL_NO_DATA) { - qWarning() << "QODBCDriver::tables failed to retrieve table/view list: (" << r << "," << qWarnODBCHandle(SQL_HANDLE_STMT, hStmt) << ")"; + r = d->sqlFetchNext(hStmt); + if (!SQL_SUCCEEDED(r) && r != SQL_NO_DATA) { + qSqlWarning("QODBCDriver::tables failed to retrieve table/view list"_L1, + hStmt.handle()); return QStringList(); } while (r == SQL_SUCCESS) { - tl.append(qGetStringData(hStmt, 2, -1, d->unicode).toString()); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + tl.append(qGetStringData(hStmt.handle(), 2, -1, d->unicode).toString()); + r = d->sqlFetchNext(hStmt); } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle"_L1 + QString::number(r), d); return tl; } @@ -2426,41 +2412,23 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const bool usingSpecialColumns = false; QSqlRecord rec = record(tablename); - SQLHANDLE hStmt; - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { - qSqlWarning("QODBCDriver::primaryIndex: Unable to list primary key"_L1, d); + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { + qSqlWarning("QODBCDriver::primaryIndex: Unable to allocate handle"_L1, d); return index; } QString catalog, schema, table; d->splitTableQualifier(tablename, catalog, schema, table); - if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) - catalog = stripDelimiters(catalog, QSqlDriver::TableName); - else - catalog = d->adjustCase(catalog); - - if (isIdentifierEscaped(schema, QSqlDriver::TableName)) - schema = stripDelimiters(schema, QSqlDriver::TableName); - else - schema = d->adjustCase(schema); - - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = d->adjustCase(table); - - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); { auto c = toSQLTCHAR(catalog); auto s = toSQLTCHAR(schema); auto t = toSQLTCHAR(table); - r = SQLPrimaryKeys(hStmt, + r = SQLPrimaryKeys(hStmt.handle(), catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), t.data(), t.size()); @@ -2473,7 +2441,7 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const auto c = toSQLTCHAR(catalog); auto s = toSQLTCHAR(schema); auto t = toSQLTCHAR(table); - r = SQLSpecialColumns(hStmt, + r = SQLSpecialColumns(hStmt.handle(), SQL_BEST_ROWID, catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), @@ -2482,44 +2450,31 @@ QSqlIndex QODBCDriver::primaryIndex(const QString& tablename) const SQL_NULLABLE); if (r != SQL_SUCCESS) { - qSqlWarning("QODBCDriver::primaryIndex: Unable to execute primary key list"_L1, d); + qSqlWarning("QODBCDriver::primaryIndex: Unable to execute primary key list"_L1, + hStmt.handle()); } else { usingSpecialColumns = true; } } - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + r = d->sqlFetchNext(hStmt); int fakeId = 0; QString cName, idxName; // Store all fields in a StringList because some drivers can't detail fields in this FETCH loop while (r == SQL_SUCCESS) { if (usingSpecialColumns) { - cName = qGetStringData(hStmt, 1, -1, d->unicode).toString(); // column name + cName = qGetStringData(hStmt.handle(), 1, -1, d->unicode).toString(); // column name idxName = QString::number(fakeId++); // invent a fake index name } else { - cName = qGetStringData(hStmt, 3, -1, d->unicode).toString(); // column name - idxName = qGetStringData(hStmt, 5, -1, d->unicode).toString(); // pk index name + cName = qGetStringData(hStmt.handle(), 3, -1, d->unicode).toString(); // column name + idxName = qGetStringData(hStmt.handle(), 5, -1, d->unicode).toString(); // pk index name } index.append(rec.field(cName)); index.setName(idxName); - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); - + r = d->sqlFetchNext(hStmt); } - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle"_L1 + QString::number(r), d); return index; } @@ -2530,41 +2485,24 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const if (!isOpen()) return fil; - SQLHANDLE hStmt; - QString catalog, schema, table; - d->splitTableQualifier(tablename, catalog, schema, table); - - if (isIdentifierEscaped(catalog, QSqlDriver::TableName)) - catalog = stripDelimiters(catalog, QSqlDriver::TableName); - else - catalog = d->adjustCase(catalog); - - if (isIdentifierEscaped(schema, QSqlDriver::TableName)) - schema = stripDelimiters(schema, QSqlDriver::TableName); - else - schema = d->adjustCase(schema); - - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - else - table = d->adjustCase(table); - - SQLRETURN r = SQLAllocHandle(SQL_HANDLE_STMT, - d->hDbc, - &hStmt); - if (r != SQL_SUCCESS) { + SqlStmtHandle hStmt(d->hDbc); + if (!hStmt.isValid()) { qSqlWarning("QODBCDriver::record: Unable to allocate handle"_L1, d); return fil; } - r = SQLSetStmtAttr(hStmt, - SQL_ATTR_CURSOR_TYPE, - (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, - SQL_IS_UINTEGER); + + QString catalog, schema, table; + d->splitTableQualifier(tablename, catalog, schema, table); + + SQLRETURN r = SQLSetStmtAttr(hStmt.handle(), + SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, + SQL_IS_UINTEGER); { auto c = toSQLTCHAR(catalog); auto s = toSQLTCHAR(schema); auto t = toSQLTCHAR(table); - r = SQLColumns(hStmt, + r = SQLColumns(hStmt.handle(), catalog.isEmpty() ? nullptr : c.data(), c.size(), schema.isEmpty() ? nullptr : s.data(), s.size(), t.data(), t.size(), @@ -2572,32 +2510,14 @@ QSqlRecord QODBCDriver::record(const QString& tablename) const 0); } if (r != SQL_SUCCESS) - qSqlWarning("QODBCDriver::record: Unable to execute column list"_L1, d); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + qSqlWarning("QODBCDriver::record: Unable to execute column list"_L1, hStmt.handle()); + r = d->sqlFetchNext(hStmt); // Store all fields in a StringList because some drivers can't detail fields in this FETCH loop while (r == SQL_SUCCESS) { - - fil.append(qMakeFieldInfo(hStmt, d)); - - if (d->hasSQLFetchScroll) - r = SQLFetchScroll(hStmt, - SQL_FETCH_NEXT, - 0); - else - r = SQLFetch(hStmt); + fil.append(qMakeFieldInfo(hStmt.handle(), d)); + r = d->sqlFetchNext(hStmt); } - - r = SQLFreeHandle(SQL_HANDLE_STMT, hStmt); - if (r!= SQL_SUCCESS) - qSqlWarning("QODBCDriver: Unable to free statement handle "_L1 + QString::number(r), d); - return fil; } @@ -2609,9 +2529,10 @@ QString QODBCDriver::formatValue(const QSqlField &field, r = "NULL"_L1; } else if (field.metaType().id() == QMetaType::QDateTime) { // Use an escape sequence for the datetime fields - if (field.value().toDateTime().isValid()){ - QDate dt = field.value().toDateTime().date(); - QTime tm = field.value().toDateTime().time(); + const QDateTime dateTime = field.value().toDateTime(); + if (dateTime.isValid()) { + const QDate dt = dateTime.date(); + const QTime tm = dateTime.time(); // Dateformat has to be "yyyy-MM-dd hh:mm:ss", with leading zeroes if month or day < 10 r = "{ ts '"_L1 + QString::number(dt.year()) + u'-' + @@ -2650,9 +2571,10 @@ QString QODBCDriver::escapeIdentifier(const QString &identifier, IdentifierType) QChar quote = const_cast<QODBCDriverPrivate*>(d)->quoteChar(); QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(quote) && !identifier.endsWith(quote) ) { - res.replace(quote, QString(quote)+QString(quote)); - res.prepend(quote).append(quote); - res.replace(u'.', QString(quote) + u'.' +QString(quote)); + const QString quoteStr(quote); + res.replace(quote, quoteStr + quoteStr); + res.replace(u'.', quoteStr + u'.' + quoteStr); + res = quote + res + quote; } return res; } diff --git a/src/plugins/sqldrivers/psql/CMakeLists.txt b/src/plugins/sqldrivers/psql/CMakeLists.txt index 269fe3367d..2f55ab4950 100644 --- a/src/plugins/sqldrivers/psql/CMakeLists.txt +++ b/src/plugins/sqldrivers/psql/CMakeLists.txt @@ -1,9 +1,7 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from psql.pro. - -qt_find_package(PostgreSQL) # special case +qt_find_package(PostgreSQL) ##################################################################### ## QPSQLDriverPlugin Plugin: @@ -18,6 +16,7 @@ qt_internal_add_plugin(QPSQLDriverPlugin DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES PostgreSQL::PostgreSQL Qt::Core @@ -25,9 +24,6 @@ qt_internal_add_plugin(QPSQLDriverPlugin Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:psql.pro:<TRUE>: -# OTHER_FILES = "psql.json" - # PostgreSQL delivers header files that are not a part of PostgreSQL itself. When precompiled # headers are processed, MinGW uses 'pthread.h' from the PostgreSQL installation directory. # As result, we disable precompile headers for the plugin. diff --git a/src/plugins/sqldrivers/psql/qsql_psql.cpp b/src/plugins/sqldrivers/psql/qsql_psql.cpp index 06a15bb14a..7b9521fec6 100644 --- a/src/plugins/sqldrivers/psql/qsql_psql.cpp +++ b/src/plugins/sqldrivers/psql/qsql_psql.cpp @@ -6,6 +6,7 @@ #include <qcoreapplication.h> #include <qvariant.h> #include <qdatetime.h> +#include <qloggingcategory.h> #include <qregularexpression.h> #include <qsqlerror.h> #include <qsqlfield.h> @@ -65,6 +66,8 @@ Q_DECLARE_METATYPE(PGresult*) QT_BEGIN_NAMESPACE +static Q_LOGGING_CATEGORY(lcPsql, "qt.sql.postgresql") + using namespace Qt::StringLiterals; inline void qPQfreemem(void *buffer) @@ -74,11 +77,11 @@ inline void qPQfreemem(void *buffer) /* Missing declaration of PGRES_SINGLE_TUPLE for PSQL below 9.2 */ #if !defined PG_VERSION_NUM || PG_VERSION_NUM-0 < 90200 -static const int PGRES_SINGLE_TUPLE = 9; +static constexpr int PGRES_SINGLE_TUPLE = 9; #endif typedef int StatementId; -static const StatementId InvalidStatementId = 0; +static constexpr StatementId InvalidStatementId = 0; class QPSQLResultPrivate; @@ -122,10 +125,9 @@ public: QSocketNotifier *sn = nullptr; QPSQLDriver::Protocol pro = QPSQLDriver::Version6; StatementId currentStmtId = InvalidStatementId; - int stmtCount = 0; + StatementId stmtCount = InvalidStatementId; mutable bool pendingNotifyCheck = false; bool hasBackslashEscape = false; - bool isUtf8 = false; void appendTables(QStringList &tl, QSqlQuery &t, QChar type); PGresult *exec(const char *stmt); @@ -141,6 +143,7 @@ public: bool setEncodingUtf8(); void setDatestyle(); void setByteaOutput(); + void setUtcTimeZone(); void detectBackslashEscape(); mutable QHash<int, QString> oidToTable; }; @@ -175,7 +178,7 @@ PGresult *QPSQLDriverPrivate::exec(const char *stmt) PGresult *QPSQLDriverPrivate::exec(const QString &stmt) { - return exec((isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); + return exec(stmt.toUtf8().constData()); } StatementId QPSQLDriverPrivate::sendQuery(const QString &stmt) @@ -183,8 +186,7 @@ StatementId QPSQLDriverPrivate::sendQuery(const QString &stmt) // Discard any prior query results that the application didn't eat. // This is required for PQsendQuery() discardResults(); - const int result = PQsendQuery(connection, - (isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); + const int result = PQsendQuery(connection, stmt.toUtf8().constData()); currentStmtId = result ? generateStatementId() : InvalidStatementId; return currentStmtId; } @@ -209,8 +211,8 @@ PGresult *QPSQLDriverPrivate::getResult(StatementId stmtId) const if (stmtId != currentStmtId) { // If you change the following warning, remember to update it // on sql-driver.html page too. - qWarning("QPSQLDriver::getResult: Query results lost - " - "probably discarded on executing another SQL query."); + qCWarning(lcPsql, "QPSQLDriver::getResult: Query results lost - " + "probably discarded on executing another SQL query."); return nullptr; } PGresult *result = PQgetResult(connection); @@ -234,7 +236,7 @@ void QPSQLDriverPrivate::discardResults() const StatementId QPSQLDriverPrivate::generateStatementId() { - int stmtId = ++stmtCount; + StatementId stmtId = ++stmtCount; if (stmtId <= 0) stmtId = stmtCount = 1; return stmtId; @@ -245,18 +247,18 @@ void QPSQLDriverPrivate::checkPendingNotifications() const Q_Q(const QPSQLDriver); if (seid.size() && !pendingNotifyCheck) { pendingNotifyCheck = true; - QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), "_q_handleNotification", Qt::QueuedConnection); + QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), &QPSQLDriver::_q_handleNotification, Qt::QueuedConnection); } } -class QPSQLResultPrivate : public QSqlResultPrivate +class QPSQLResultPrivate final : public QSqlResultPrivate { Q_DECLARE_PUBLIC(QPSQLResult) public: Q_DECLARE_SQLDRIVER_PRIVATE(QPSQLDriver) using QSqlResultPrivate::QSqlResultPrivate; - QString fieldSerial(qsizetype i) const override { return u'$' + QString::number(i + 1); } + QString fieldSerial(qsizetype i) const override { return QString("$%1"_L1).arg(i + 1); } void deallocatePreparedStmt(); std::queue<PGresult*> nextResultSets; @@ -274,7 +276,7 @@ static QSqlError qMakeError(const QString &err, QSqlError::ErrorType type, const QPSQLDriverPrivate *p, PGresult *result = nullptr) { const char *s = PQerrorMessage(p->connection); - QString msg = p->isUtf8 ? QString::fromUtf8(s) : QString::fromLocal8Bit(s); + QString msg = QString::fromUtf8(s); QString errorCode; if (result) { errorCode = QString::fromLatin1(PQresultErrorField(result, PG_DIAG_SQLSTATE)); @@ -382,8 +384,10 @@ void QPSQLResultPrivate::deallocatePreparedStmt() const QString stmt = QStringLiteral("DEALLOCATE ") + preparedStmtId; PGresult *result = drv_d_func()->exec(stmt); - if (PQresultStatus(result) != PGRES_COMMAND_OK) - qWarning("Unable to free statement: %s", PQerrorMessage(drv_d_func()->connection)); + if (PQresultStatus(result) != PGRES_COMMAND_OK) { + const QString msg = QString::fromUtf8(PQerrorMessage(drv_d_func()->connection)); + qCWarning(lcPsql, "Unable to free statement: %ls.", qUtf16Printable(msg)); + } PQclear(result); } preparedStmtId.clear(); @@ -593,7 +597,7 @@ QVariant QPSQLResult::data(int i) { Q_D(const QPSQLResult); if (i >= PQnfields(d->result)) { - qWarning("QPSQLResult::data: column %d out of range", i); + qCWarning(lcPsql, "QPSQLResult::data: column %d out of range.", i); return QVariant(); } const int currentRow = isForwardOnly() ? 0 : at(); @@ -606,7 +610,7 @@ QVariant QPSQLResult::data(int i) case QMetaType::Bool: return QVariant((bool)(val[0] == 't')); case QMetaType::QString: - return d->drv_d_func()->isUtf8 ? QString::fromUtf8(val) : QString::fromLatin1(val); + return QString::fromUtf8(val); case QMetaType::LongLong: if (val[0] == '-') return QByteArray::fromRawData(val, qstrlen(val)).toLongLong(); @@ -641,23 +645,21 @@ QVariant QPSQLResult::data(int i) } return dbl; } - case QMetaType::QDate: #if QT_CONFIG(datestring) + case QMetaType::QDate: return QVariant(QDate::fromString(QString::fromLatin1(val), Qt::ISODate)); -#else - return QVariant(QString::fromLatin1(val)); -#endif case QMetaType::QTime: -#if QT_CONFIG(datestring) return QVariant(QTime::fromString(QString::fromLatin1(val), Qt::ISODate)); + case QMetaType::QDateTime: { + QString tzString(QString::fromLatin1(val)); + if (!tzString.endsWith(u'Z')) + tzString.append(u'Z'); // make UTC + return QVariant(QDateTime::fromString(tzString, Qt::ISODate)); + } #else - return QVariant(QString::fromLatin1(val)); -#endif + case QMetaType::QDate: + case QMetaType::QTime: case QMetaType::QDateTime: -#if QT_CONFIG(datestring) - return QVariant(QDateTime::fromString(QString::fromLatin1(val), - Qt::ISODate).toLocalTime()); -#else return QVariant(QString::fromLatin1(val)); #endif case QMetaType::QByteArray: { @@ -668,7 +670,7 @@ QVariant QPSQLResult::data(int i) return QVariant(ba); } default: - qWarning("QPSQLResult::data: unknown data type"); + qCWarning(lcPsql, "QPSQLResult::data: unhandled data type %d.", type.id()); } return QVariant(); } @@ -747,10 +749,7 @@ QSqlRecord QPSQLResult::record() const int count = PQnfields(d->result); QSqlField f; for (int i = 0; i < count; ++i) { - if (d->drv_d_func()->isUtf8) - f.setName(QString::fromUtf8(PQfname(d->result, i))); - else - f.setName(QString::fromLocal8Bit(PQfname(d->result, i))); + f.setName(QString::fromUtf8(PQfname(d->result, i))); const int tableOid = PQftable(d->result, i); // WARNING: We cannot execute any other SQL queries on // the same db connection while forward-only mode is active @@ -801,7 +800,6 @@ QSqlRecord QPSQLResult::record() const f.setLength(len); f.setPrecision(precision); - f.setSqlType(ptype); info.append(f); } return info; @@ -918,7 +916,7 @@ void QPSQLDriverPrivate::setDatestyle() PGresult *result = exec("SET DATESTYLE TO 'ISO'"); int status = PQresultStatus(result); if (status != PGRES_COMMAND_OK) - qWarning("%s", PQerrorMessage(connection)); + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); PQclear(result); } @@ -931,11 +929,20 @@ void QPSQLDriverPrivate::setByteaOutput() PGresult *result = exec("SET bytea_output TO escape"); int status = PQresultStatus(result); if (status != PGRES_COMMAND_OK) - qWarning("%s", PQerrorMessage(connection)); + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); PQclear(result); } } +void QPSQLDriverPrivate::setUtcTimeZone() +{ + PGresult *result = exec("SET TIME ZONE 'UTC'"); + int status = PQresultStatus(result); + if (status != PGRES_COMMAND_OK) + qCWarning(lcPsql) << QString::fromUtf8(PQerrorMessage(connection)); + PQclear(result); +} + void QPSQLDriverPrivate::detectBackslashEscape() { // standard_conforming_strings option introduced in 8.2 @@ -1069,16 +1076,16 @@ QPSQLDriver::Protocol QPSQLDriverPrivate::getPSQLVersion() if (serverVersion == QPSQLDriver::VersionUnknown) { serverVersion = clientVersion; if (serverVersion != QPSQLDriver::VersionUnknown) - qWarning("The server version of this PostgreSQL is unknown, falling back to the client version."); + qCWarning(lcPsql, "The server version of this PostgreSQL is unknown, " + "falling back to the client version."); } // Keep the old behavior unchanged if (serverVersion == QPSQLDriver::VersionUnknown) serverVersion = QPSQLDriver::Version6; - if (serverVersion < QPSQLDriver::Version7_3) { - qWarning("This version of PostgreSQL is not supported and may not work."); - } + if (serverVersion < QPSQLDriver::Version7_3) + qCWarning(lcPsql, "This version of PostgreSQL is not supported and may not work."); return serverVersion; } @@ -1104,8 +1111,7 @@ QPSQLDriver::QPSQLDriver(PGconn *conn, QObject *parent) QPSQLDriver::~QPSQLDriver() { Q_D(QPSQLDriver); - if (d->connection) - PQfinish(d->connection); + PQfinish(d->connection); } QVariant QPSQLDriver::handle() const @@ -1125,6 +1131,7 @@ bool QPSQLDriver::hasFeature(DriverFeature f) const case EventNotifications: case MultipleResultSets: case BLOB: + case Unicode: return true; case PreparedQueries: case PositionalPlaceholders: @@ -1135,8 +1142,6 @@ bool QPSQLDriver::hasFeature(DriverFeature f) const case FinishQuery: case CancelQuery: return false; - case Unicode: - return d->isUtf8; } return false; } @@ -1194,9 +1199,16 @@ bool QPSQLDriver::open(const QString &db, d->pro = d->getPSQLVersion(); d->detectBackslashEscape(); - d->isUtf8 = d->setEncodingUtf8(); + if (!d->setEncodingUtf8()) { + setLastError(qMakeError(tr("Unable to set client encoding to 'UNICODE'"), QSqlError::ConnectionError, d)); + setOpenError(true); + PQfinish(d->connection); + d->connection = nullptr; + return false; + } d->setDatestyle(); d->setByteaOutput(); + d->setUtcTimeZone(); setOpen(true); setOpenError(false); @@ -1209,13 +1221,12 @@ void QPSQLDriver::close() d->seid.clear(); if (d->sn) { - disconnect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + disconnect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); delete d->sn; d->sn = nullptr; } - if (d->connection) - PQfinish(d->connection); + PQfinish(d->connection); d->connection = nullptr; setOpen(false); setOpenError(false); @@ -1230,7 +1241,7 @@ bool QPSQLDriver::beginTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::beginTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::beginTransaction: Database not open."); return false; } PGresult *res = d->exec("BEGIN"); @@ -1248,7 +1259,7 @@ bool QPSQLDriver::commitTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::commitTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::commitTransaction: Database not open."); return false; } PGresult *res = d->exec("COMMIT"); @@ -1277,7 +1288,7 @@ bool QPSQLDriver::rollbackTransaction() { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::rollbackTransaction: Database not open"); + qCWarning(lcPsql, "QPSQLDriver::rollbackTransaction: Database not open."); return false; } PGresult *res = d->exec("ROLLBACK"); @@ -1414,7 +1425,6 @@ QSqlRecord QPSQLDriver::record(const QString &tablename) const f.setLength(len); f.setPrecision(precision); f.setDefaultValue(defVal); - f.setSqlType(query.value(1).toInt()); info.append(f); } @@ -1439,32 +1449,28 @@ QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const r = nullStr(); } else { switch (field.metaType().id()) { - case QMetaType::QDateTime: -#if QT_CONFIG(datestring) - if (field.value().toDateTime().isValid()) { + case QMetaType::QDateTime: { + const auto dt = field.value().toDateTime(); + if (dt.isValid()) { // we force the value to be considered with a timezone information, and we force it to be UTC // this is safe since postgresql stores only the UTC value and not the timezone offset (only used // while parsing), so we have correct behavior in both case of with timezone and without tz r = QStringLiteral("TIMESTAMP WITH TIME ZONE ") + u'\'' + - QLocale::c().toString(field.value().toDateTime().toUTC(), u"yyyy-MM-ddThh:mm:ss.zzz") + + QLocale::c().toString(dt.toUTC(), u"yyyy-MM-ddThh:mm:ss.zzz") + u'Z' + u'\''; } else { r = nullStr(); } -#else - r = nullStr(); -#endif // datestring break; - case QMetaType::QTime: -#if QT_CONFIG(datestring) - if (field.value().toTime().isValid()) { - r = u'\'' + field.value().toTime().toString(u"hh:mm:ss.zzz") + u'\''; - } else -#endif - { + } + case QMetaType::QTime: { + const auto t = field.value().toTime(); + if (t.isValid()) + r = u'\'' + QLocale::c().toString(t, u"hh:mm:ss.zzz") + u'\''; + else r = nullStr(); - } break; + } case QMetaType::QString: r = QSqlDriver::formatValue(field, trimStrings); if (d->hasBackslashEscape) @@ -1516,8 +1522,8 @@ QString QPSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) QString res = identifier; if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"') ) { res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); res.replace(u'.', "\".\""_L1); + res = u'"' + res + u'"'; } return res; } @@ -1538,7 +1544,7 @@ bool QPSQLDriver::subscribeToNotification(const QString &name) { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::subscribeToNotificationImplementation: database not open."); + qCWarning(lcPsql, "QPSQLDriver::subscribeToNotification: Database not open."); return false; } @@ -1563,11 +1569,12 @@ bool QPSQLDriver::subscribeToNotification(const QString &name) PQclear(result); if (!d->sn) { - d->sn = new QSocketNotifier(socket, QSocketNotifier::Read); - connect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + d->sn = new QSocketNotifier(socket, QSocketNotifier::Read, this); + connect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); } } else { - qWarning("QPSQLDriver::subscribeToNotificationImplementation: PQsocket didn't return a valid socket to listen on"); + qCWarning(lcPsql, "QPSQLDriver::subscribeToNotificationImplementation: " + "PQsocket didn't return a valid socket to listen on."); return false; } @@ -1578,13 +1585,13 @@ bool QPSQLDriver::unsubscribeFromNotification(const QString &name) { Q_D(QPSQLDriver); if (!isOpen()) { - qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: database not open."); + qCWarning(lcPsql, "QPSQLDriver::unsubscribeFromNotification: Database not open."); return false; } if (!d->seid.contains(name)) { - qWarning("QPSQLDriver::unsubscribeFromNotificationImplementation: not subscribed to '%s'.", - qPrintable(name)); + qCWarning(lcPsql, "QPSQLDriver::unsubscribeFromNotification: not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1600,7 +1607,7 @@ bool QPSQLDriver::unsubscribeFromNotification(const QString &name) d->seid.removeAll(name); if (d->seid.isEmpty()) { - disconnect(d->sn, SIGNAL(activated(QSocketDescriptor)), this, SLOT(_q_handleNotification())); + disconnect(d->sn, &QSocketNotifier::activated, this, &QPSQLDriver::_q_handleNotification); delete d->sn; d->sn = nullptr; } @@ -1627,17 +1634,19 @@ void QPSQLDriver::_q_handleNotification() QString payload; #if defined PG_VERSION_NUM && PG_VERSION_NUM-0 >= 70400 if (notify->extra) - payload = d->isUtf8 ? QString::fromUtf8(notify->extra) : QString::fromLatin1(notify->extra); + payload = QString::fromUtf8(notify->extra); #endif QSqlDriver::NotificationSource source = (notify->be_pid == PQbackendPID(d->connection)) ? QSqlDriver::SelfSource : QSqlDriver::OtherSource; emit notification(name, source, payload); } else - qWarning("QPSQLDriver: received notification for '%s' which isn't subscribed to.", - qPrintable(name)); + qCWarning(lcPsql, "QPSQLDriver: received notification for '%ls' which isn't subscribed to.", + qUtf16Printable(name)); qPQfreemem(notify); } } QT_END_NAMESPACE + +#include "moc_qsql_psql_p.cpp" diff --git a/src/plugins/sqldrivers/qt_cmdline.cmake b/src/plugins/sqldrivers/qt_cmdline.cmake index 8116a5cdae..2bb1fc64eb 100644 --- a/src/plugins/sqldrivers/qt_cmdline.cmake +++ b/src/plugins/sqldrivers/qt_cmdline.cmake @@ -3,7 +3,7 @@ qt_commandline_option(mysql_config TYPE string) qt_commandline_option(psql_config TYPE string) -qt_commandline_option(sqlite TYPE enum NAME system-sqlite MAPPING qt no system yes) +qt_commandline_option(sqlite CONTROLS_FEATURE TYPE enum NAME system-sqlite MAPPING qt no system yes) qt_commandline_option(sql-db2 TYPE boolean) qt_commandline_option(sql-ibase TYPE boolean) qt_commandline_option(sql-mysql TYPE boolean) @@ -11,6 +11,7 @@ qt_commandline_option(sql-oci TYPE boolean) qt_commandline_option(sql-odbc TYPE boolean) qt_commandline_option(sql-psql TYPE boolean) qt_commandline_option(sql-sqlite TYPE boolean) +qt_commandline_option(sql-mimer TYPE boolean) qt_commandline_option(plugin-sql-db2 TYPE void NAME sql-db2) qt_commandline_option(plugin-sql-ibase TYPE void NAME sql-ibase) qt_commandline_option(plugin-sql-mysql TYPE void NAME sql-mysql) @@ -18,3 +19,4 @@ qt_commandline_option(plugin-sql-oci TYPE void NAME sql-oci) qt_commandline_option(plugin-sql-odbc TYPE void NAME sql-odbc) qt_commandline_option(plugin-sql-psql TYPE void NAME sql-psql) qt_commandline_option(plugin-sql-sqlite TYPE void NAME sql-sqlite) +qt_commandline_option(plugin-sql-mimer TYPE void NAME sql-mimer) diff --git a/src/plugins/sqldrivers/sqlite/CMakeLists.txt b/src/plugins/sqldrivers/sqlite/CMakeLists.txt index a183ecc965..4203a5c437 100644 --- a/src/plugins/sqldrivers/sqlite/CMakeLists.txt +++ b/src/plugins/sqldrivers/sqlite/CMakeLists.txt @@ -1,8 +1,6 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: BSD-3-Clause -# Generated from sqlite.pro. - ##################################################################### ## QSQLiteDriverPlugin Plugin: ##################################################################### @@ -12,20 +10,18 @@ qt_internal_add_plugin(QSQLiteDriverPlugin PLUGIN_TYPE sqldrivers SOURCES qsql_sqlite.cpp qsql_sqlite_p.h + qsql_sqlite_vfs.cpp qsql_sqlite_vfs_p.h smain.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII + QT_NO_CONTEXTLESS_CONNECT LIBRARIES Qt::Core Qt::CorePrivate Qt::SqlPrivate ) -#### Keys ignored in scope 1:.:.:sqlite.pro:<TRUE>: -# OTHER_FILES = "sqlite.json" -# QT_FOR_CONFIG = "sqldrivers-private" - ## Scopes: ##################################################################### @@ -34,7 +30,6 @@ qt_internal_extend_target(QSQLiteDriverPlugin CONDITION QT_FEATURE_system_sqlite SQLite::SQLite3 ) -# special case begin if (NOT QT_FEATURE_system_sqlite) # On newer compilers compiling sqlite.c produces warnings qt_disable_warnings(QSQLiteDriverPlugin) @@ -43,7 +38,11 @@ endif() if(QT_FEATURE_system_sqlite) qt_internal_force_macos_intel_arch(QSQLiteDriverPlugin) endif() -# special case end + +qt_internal_extend_target(QSQLiteDriverPlugin CONDITION NOT QT_FEATURE_system_sqlite AND VXWORKS + DEFINES + SQLITE_OS_UNIX=1 +) qt_internal_extend_target(QSQLiteDriverPlugin CONDITION NOT QT_FEATURE_system_sqlite SOURCES diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp index 6812addae1..c574772fd7 100644 --- a/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite.cpp @@ -7,6 +7,7 @@ #include <qdatetime.h> #include <qdebug.h> #include <qlist.h> +#include <qloggingcategory.h> #include <qsqlerror.h> #include <qsqlfield.h> #include <qsqlindex.h> @@ -38,23 +39,9 @@ Q_DECLARE_METATYPE(sqlite3_stmt*) QT_BEGIN_NAMESPACE -using namespace Qt::StringLiterals; +static Q_LOGGING_CATEGORY(lcSqlite, "qt.sql.sqlite") -static QString _q_escapeIdentifier(const QString &identifier, QSqlDriver::IdentifierType type) -{ - QString res = identifier; - // If it contains [ and ] then we assume it to be escaped properly already as this indicates - // the syntax is exactly how it should be - if (identifier.contains(u'[') && identifier.contains(u']')) - return res; - if (!identifier.isEmpty() && !identifier.startsWith(u'"') && !identifier.endsWith(u'"')) { - res.replace(u'"', "\"\""_L1); - res.prepend(u'"').append(u'"'); - if (type == QSqlDriver::TableName) - res.replace(u'.', "\".\""_L1); - } - return res; -} +using namespace Qt::StringLiterals; static int qGetColumnType(const QString &tpName) { @@ -114,11 +101,64 @@ class QSQLiteDriverPrivate : public QSqlDriverPrivate public: inline QSQLiteDriverPrivate() : QSqlDriverPrivate(QSqlDriver::SQLite) {} + bool isIdentifierEscaped(QStringView identifier) const; + QSqlIndex getTableInfo(QSqlQuery &query, const QString &tableName, + bool onlyPIndex = false) const; + sqlite3 *access = nullptr; QList<QSQLiteResult *> results; QStringList notificationid; }; +bool QSQLiteDriverPrivate::isIdentifierEscaped(QStringView identifier) const +{ + return identifier.size() > 2 + && ((identifier.startsWith(u'"') && identifier.endsWith(u'"')) + || (identifier.startsWith(u'`') && identifier.endsWith(u'`')) + || (identifier.startsWith(u'[') && identifier.endsWith(u']'))); +} + +QSqlIndex QSQLiteDriverPrivate::getTableInfo(QSqlQuery &query, const QString &tableName, + bool onlyPIndex) const +{ + Q_Q(const QSQLiteDriver); + QString schema; + QString table = q->escapeIdentifier(tableName, QSqlDriver::TableName); + const auto indexOfSeparator = table.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{table}.first(indexOfSeparator); + auto rightName = QStringView{table}.sliced(indexOfSeparator + 1); + if (isIdentifierEscaped(leftName) && isIdentifierEscaped(rightName)) { + schema = leftName.toString() + u'.'; + table = rightName.toString(); + } + } + + query.exec("PRAGMA "_L1 + schema + "table_info ("_L1 + table + u')'); + QSqlIndex ind; + while (query.next()) { + bool isPk = query.value(5).toInt(); + if (onlyPIndex && !isPk) + continue; + QString typeName = query.value(2).toString().toLower(); + QString defVal = query.value(4).toString(); + if (!defVal.isEmpty() && defVal.at(0) == u'\'') { + const int end = defVal.lastIndexOf(u'\''); + if (end > 0) + defVal = defVal.mid(1, end - 1); + } + + QSqlField fld(query.value(1).toString(), QMetaType(qGetColumnType(typeName)), tableName); + if (isPk && (typeName == "integer"_L1)) + // INTEGER PRIMARY KEY fields are auto-generated in sqlite + // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! + fld.setAutoValue(true); + fld.setRequired(query.value(3).toInt() != 0); + fld.setDefaultValue(defVal); + ind.append(fld); + } + return ind; +} class QSQLiteResultPrivate : public QSqlCachedResultPrivate { @@ -210,7 +250,6 @@ void QSQLiteResultPrivate::initColumns(bool emptyResultset) } QSqlField fld(colName, QMetaType(fieldType), tableName); - fld.setSqlType(stp); rInf.append(fld); } } @@ -625,6 +664,30 @@ static void _q_regexp_cleanup(void *cache) } #endif +static void _q_lower(sqlite3_context* context, int argc, sqlite3_value** argv) +{ + if (Q_UNLIKELY(argc != 1)) { + sqlite3_result_text(context, nullptr, 0, nullptr); + return; + } + const QString lower = QString::fromUtf8( + reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toLower(); + const QByteArray ba = lower.toUtf8(); + sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT); +} + +static void _q_upper(sqlite3_context* context, int argc, sqlite3_value** argv) +{ + if (Q_UNLIKELY(argc != 1)) { + sqlite3_result_text(context, nullptr, 0, nullptr); + return; + } + const QString upper = QString::fromUtf8( + reinterpret_cast<const char*>(sqlite3_value_text(argv[0]))).toUpper(); + const QByteArray ba = upper.toUtf8(); + sqlite3_result_text(context, ba.data(), ba.size(), SQLITE_TRANSIENT); +} + QSQLiteDriver::QSQLiteDriver(QObject * parent) : QSqlDriver(*new QSQLiteDriverPrivate, parent) { @@ -691,13 +754,16 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c bool openReadOnlyOption = false; bool openUriOption = false; bool useExtendedResultCodes = true; + bool useQtVfs = false; + bool useQtCaseFolding = false; + bool openNoFollow = false; #if QT_CONFIG(regularexpression) static const auto regexpConnectOption = "QSQLITE_ENABLE_REGEXP"_L1; bool defineRegexp = false; int regexpCacheSize = 25; #endif - const auto opts = QStringView{conOpts}.split(u';'); + const auto opts = QStringView{conOpts}.split(u';', Qt::SkipEmptyParts); for (auto option : opts) { option = option.trimmed(); if (option.startsWith("QSQLITE_BUSY_TIMEOUT"_L1)) { @@ -708,6 +774,8 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c if (ok) timeOut = nt; } + } else if (option == "QSQLITE_USE_QT_VFS"_L1) { + useQtVfs = true; } else if (option == "QSQLITE_OPEN_READONLY"_L1) { openReadOnlyOption = true; } else if (option == "QSQLITE_OPEN_URI"_L1) { @@ -716,6 +784,10 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c sharedCache = true; } else if (option == "QSQLITE_NO_USE_EXTENDED_RESULT_CODES"_L1) { useExtendedResultCodes = false; + } else if (option == "QSQLITE_ENABLE_NON_ASCII_CASE_FOLDING"_L1) { + useQtCaseFolding = true; + } else if (option == "QSQLITE_OPEN_NOFOLLOW"_L1) { + openNoFollow = true; } #if QT_CONFIG(regularexpression) else if (option.startsWith(regexpConnectOption)) { @@ -733,16 +805,25 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c } } #endif + else + qCWarning(lcSqlite, "Unsupported option '%ls'", qUtf16Printable(option.toString())); } int openMode = (openReadOnlyOption ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); openMode |= (sharedCache ? SQLITE_OPEN_SHAREDCACHE : SQLITE_OPEN_PRIVATECACHE); if (openUriOption) openMode |= SQLITE_OPEN_URI; + if (openNoFollow) { +#if defined(SQLITE_OPEN_NOFOLLOW) + openMode |= SQLITE_OPEN_NOFOLLOW; +#else + qCWarning(lcSqlite, "SQLITE_OPEN_NOFOLLOW not supported with the SQLite version %s", sqlite3_libversion()); +#endif + } openMode |= SQLITE_OPEN_NOMUTEX; - const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, nullptr); + const int res = sqlite3_open_v2(db.toUtf8().constData(), &d->access, openMode, useQtVfs ? "QtVFS" : nullptr); if (res == SQLITE_OK) { sqlite3_busy_timeout(d->access, timeOut); @@ -757,6 +838,12 @@ bool QSQLiteDriver::open(const QString & db, const QString &, const QString &, c nullptr, &_q_regexp_cleanup); } #endif + if (useQtCaseFolding) { + sqlite3_create_function_v2(d->access, "lower", 1, SQLITE_UTF8, nullptr, + &_q_lower, nullptr, nullptr, nullptr); + sqlite3_create_function_v2(d->access, "upper", 1, SQLITE_UTF8, nullptr, + &_q_upper, nullptr, nullptr, nullptr); + } return true; } else { setLastError(qMakeError(d->access, tr("Error opening database"), @@ -877,79 +964,26 @@ QStringList QSQLiteDriver::tables(QSql::TableType type) const return res; } -static QSqlIndex qGetTableInfo(QSqlQuery &q, const QString &tableName, bool onlyPIndex = false) -{ - QString schema; - QString table(tableName); - const qsizetype indexOfSeparator = tableName.indexOf(u'.'); - if (indexOfSeparator > -1) { - const qsizetype indexOfCloseBracket = tableName.indexOf(u']'); - if (indexOfCloseBracket != tableName.size() - 1) { - // Handles a case like databaseName.tableName - schema = tableName.left(indexOfSeparator + 1); - table = tableName.mid(indexOfSeparator + 1); - } else { - const qsizetype indexOfOpenBracket = tableName.lastIndexOf(u'[', indexOfCloseBracket); - if (indexOfOpenBracket > 0) { - // Handles a case like databaseName.[tableName] - schema = tableName.left(indexOfOpenBracket); - table = tableName.mid(indexOfOpenBracket); - } - } - } - q.exec("PRAGMA "_L1 + schema + "table_info ("_L1 + - _q_escapeIdentifier(table, QSqlDriver::TableName) + u')'); - QSqlIndex ind; - while (q.next()) { - bool isPk = q.value(5).toInt(); - if (onlyPIndex && !isPk) - continue; - QString typeName = q.value(2).toString().toLower(); - QString defVal = q.value(4).toString(); - if (!defVal.isEmpty() && defVal.at(0) == u'\'') { - const int end = defVal.lastIndexOf(u'\''); - if (end > 0) - defVal = defVal.mid(1, end - 1); - } - - QSqlField fld(q.value(1).toString(), QMetaType(qGetColumnType(typeName)), tableName); - if (isPk && (typeName == "integer"_L1)) - // INTEGER PRIMARY KEY fields are auto-generated in sqlite - // INT PRIMARY KEY is not the same as INTEGER PRIMARY KEY! - fld.setAutoValue(true); - fld.setRequired(q.value(3).toInt() != 0); - fld.setDefaultValue(defVal); - ind.append(fld); - } - return ind; -} - -QSqlIndex QSQLiteDriver::primaryIndex(const QString &tblname) const +QSqlIndex QSQLiteDriver::primaryIndex(const QString &tablename) const { + Q_D(const QSQLiteDriver); if (!isOpen()) return QSqlIndex(); - QString table = tblname; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - QSqlQuery q(createResult()); q.setForwardOnly(true); - return qGetTableInfo(q, table, true); + return d->getTableInfo(q, tablename, true); } -QSqlRecord QSQLiteDriver::record(const QString &tbl) const +QSqlRecord QSQLiteDriver::record(const QString &tablename) const { + Q_D(const QSQLiteDriver); if (!isOpen()) return QSqlRecord(); - QString table = tbl; - if (isIdentifierEscaped(table, QSqlDriver::TableName)) - table = stripDelimiters(table, QSqlDriver::TableName); - QSqlQuery q(createResult()); q.setForwardOnly(true); - return qGetTableInfo(q, table); + return d->getTableInfo(q, tablename); } QVariant QSQLiteDriver::handle() const @@ -960,7 +994,52 @@ QVariant QSQLiteDriver::handle() const QString QSQLiteDriver::escapeIdentifier(const QString &identifier, IdentifierType type) const { - return _q_escapeIdentifier(identifier, type); + Q_D(const QSQLiteDriver); + if (identifier.isEmpty() || isIdentifierEscaped(identifier, type)) + return identifier; + + const auto indexOfSeparator = identifier.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{identifier}.first(indexOfSeparator); + auto rightName = QStringView{identifier}.sliced(indexOfSeparator + 1); + const QStringView leftEnclose = d->isIdentifierEscaped(leftName) ? u"" : u"\""; + const QStringView rightEnclose = d->isIdentifierEscaped(rightName) ? u"" : u"\""; + if (leftEnclose.isEmpty() || rightEnclose.isEmpty()) + return (leftEnclose + leftName + leftEnclose + u'.' + rightEnclose + rightName + + rightEnclose); + } + return u'"' + identifier + u'"'; +} + +bool QSQLiteDriver::isIdentifierEscaped(const QString &identifier, IdentifierType type) const +{ + Q_D(const QSQLiteDriver); + Q_UNUSED(type); + return d->isIdentifierEscaped(QStringView{identifier}); +} + +QString QSQLiteDriver::stripDelimiters(const QString &identifier, IdentifierType type) const +{ + Q_D(const QSQLiteDriver); + const auto indexOfSeparator = identifier.indexOf(u'.'); + if (indexOfSeparator > -1) { + auto leftName = QStringView{identifier}.first(indexOfSeparator); + auto rightName = QStringView{identifier}.sliced(indexOfSeparator + 1); + const auto leftEscaped = d->isIdentifierEscaped(leftName); + const auto rightEscaped = d->isIdentifierEscaped(rightName); + if (leftEscaped || rightEscaped) { + if (leftEscaped) + leftName = leftName.sliced(1).chopped(1); + if (rightEscaped) + rightName = rightName.sliced(1).chopped(1); + return leftName + u'.' + rightName; + } + } + + if (isIdentifierEscaped(identifier, type)) + return identifier.mid(1, identifier.size() - 2); + + return identifier; } static void handle_sqlite_callback(void *qobj,int aoperation, char const *adbname, char const *atablename, @@ -979,12 +1058,13 @@ bool QSQLiteDriver::subscribeToNotification(const QString &name) { Q_D(QSQLiteDriver); if (!isOpen()) { - qWarning("Database not open."); + qCWarning(lcSqlite, "QSQLiteDriver::subscribeToNotification: Database not open."); return false; } if (d->notificationid.contains(name)) { - qWarning("Already subscribing to '%s'.", qPrintable(name)); + qCWarning(lcSqlite, "QSQLiteDriver::subscribeToNotification: Already subscribing to '%ls'.", + qUtf16Printable(name)); return false; } @@ -1000,12 +1080,13 @@ bool QSQLiteDriver::unsubscribeFromNotification(const QString &name) { Q_D(QSQLiteDriver); if (!isOpen()) { - qWarning("Database not open."); + qCWarning(lcSqlite, "QSQLiteDriver::unsubscribeFromNotification: Database not open."); return false; } if (!d->notificationid.contains(name)) { - qWarning("Not subscribed to '%s'.", qPrintable(name)); + qCWarning(lcSqlite, "QSQLiteDriver::unsubscribeFromNotification: Not subscribed to '%ls'.", + qUtf16Printable(name)); return false; } diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h b/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h index 53ffb45f96..db6b76cb69 100644 --- a/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_p.h @@ -54,9 +54,12 @@ public: QStringList tables(QSql::TableType) const override; QSqlRecord record(const QString& tablename) const override; - QSqlIndex primaryIndex(const QString &table) const override; + QSqlIndex primaryIndex(const QString &tablename) const override; QVariant handle() const override; - QString escapeIdentifier(const QString &identifier, IdentifierType) const override; + + QString escapeIdentifier(const QString &identifier, IdentifierType type) const override; + bool isIdentifierEscaped(const QString &identifier, IdentifierType type) const override; + QString stripDelimiters(const QString &identifier, IdentifierType type) const override; bool subscribeToNotification(const QString &name) override; bool unsubscribeFromNotification(const QString &name) override; diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp new file mode 100644 index 0000000000..bbba3cd14f --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs.cpp @@ -0,0 +1,258 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qsql_sqlite_vfs_p.h" + +#include <QFile> + +#include <limits.h> // defines PATH_MAX on unix +#include <sqlite3.h> +#include <stdio.h> // defines FILENAME_MAX everywhere + +#ifndef PATH_MAX +# define PATH_MAX FILENAME_MAX +#endif +#if SQLITE_VERSION_NUMBER < 3040000 +typedef const char *sqlite3_filename; +#endif + +namespace { +struct Vfs : sqlite3_vfs { + sqlite3_vfs *pVfs; + sqlite3_io_methods ioMethods; +}; + +struct File : sqlite3_file { + class QtFile : public QFile { + public: + QtFile(const QString &name, bool removeOnClose) + : QFile(name) + , removeOnClose(removeOnClose) + {} + + ~QtFile() override + { + if (removeOnClose) + remove(); + } + private: + bool removeOnClose; + }; + QtFile *pFile; +}; + + +int xClose(sqlite3_file *sfile) +{ + auto file = static_cast<File *>(sfile); + delete file->pFile; + file->pFile = nullptr; + return SQLITE_OK; +} + +int xRead(sqlite3_file *sfile, void *ptr, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_READ; + + auto sz = file->pFile->read(static_cast<char *>(ptr), iAmt); + if (sz < iAmt) { + memset(static_cast<char *>(ptr) + sz, 0, size_t(iAmt - sz)); + return SQLITE_IOERR_SHORT_READ; + } + return SQLITE_OK; +} + +int xWrite(sqlite3_file *sfile, const void *data, int iAmt, sqlite3_int64 iOfst) +{ + auto file = static_cast<File *>(sfile); + if (!file->pFile->seek(iOfst)) + return SQLITE_IOERR_SEEK; + return file->pFile->write(reinterpret_cast<const char*>(data), iAmt) == iAmt ? SQLITE_OK : SQLITE_IOERR_WRITE; +} + +int xTruncate(sqlite3_file *sfile, sqlite3_int64 size) +{ + auto file = static_cast<File *>(sfile); + return file->pFile->resize(size) ? SQLITE_OK : SQLITE_IOERR_TRUNCATE; +} + +int xSync(sqlite3_file *sfile, int /*flags*/) +{ + static_cast<File *>(sfile)->pFile->flush(); + return SQLITE_OK; +} + +int xFileSize(sqlite3_file *sfile, sqlite3_int64 *pSize) +{ + auto file = static_cast<File *>(sfile); + *pSize = file->pFile->size(); + return SQLITE_OK; +} + +// No lock/unlock for QFile, QLockFile doesn't work for me + +int xLock(sqlite3_file *, int) { return SQLITE_OK; } + +int xUnlock(sqlite3_file *, int) { return SQLITE_OK; } + +int xCheckReservedLock(sqlite3_file *, int *pResOut) +{ + *pResOut = 0; + return SQLITE_OK; +} + +int xFileControl(sqlite3_file *, int, void *) { return SQLITE_NOTFOUND; } + +int xSectorSize(sqlite3_file *) +{ + return 4096; +} + +int xDeviceCharacteristics(sqlite3_file *) +{ + return 0; // no SQLITE_IOCAP_XXX +} + +int xOpen(sqlite3_vfs *svfs, sqlite3_filename zName, sqlite3_file *sfile, + int flags, int *pOutFlags) +{ + auto vfs = static_cast<Vfs *>(svfs); + auto file = static_cast<File *>(sfile); + memset(file, 0, sizeof(File)); + QIODeviceBase::OpenMode mode = QIODeviceBase::NotOpen; + if (!zName || (flags & SQLITE_OPEN_MEMORY)) + return SQLITE_PERM; + if ((flags & SQLITE_OPEN_READONLY) && + !(flags & SQLITE_OPEN_READWRITE) && + !(flags & SQLITE_OPEN_CREATE) && + !(flags & SQLITE_OPEN_DELETEONCLOSE)) { + mode |= QIODeviceBase::OpenModeFlag::ReadOnly; + } else { + /* + ** ^The [SQLITE_OPEN_EXCLUSIVE] flag is always used in conjunction + ** with the [SQLITE_OPEN_CREATE] flag, which are both directly + ** analogous to the O_EXCL and O_CREAT flags of the POSIX open() + ** API. The SQLITE_OPEN_EXCLUSIVE flag, when paired with the + ** SQLITE_OPEN_CREATE, is used to indicate that file should always + ** be created, and that it is an error if it already exists. + ** It is <i>not</i> used to indicate the file should be opened + ** for exclusive access. + */ + if ((flags & SQLITE_OPEN_CREATE) && (flags & SQLITE_OPEN_EXCLUSIVE)) + mode |= QIODeviceBase::OpenModeFlag::NewOnly; + + if (flags & SQLITE_OPEN_READWRITE) + mode |= QIODeviceBase::OpenModeFlag::ReadWrite; + } + + file->pMethods = &vfs->ioMethods; + file->pFile = new File::QtFile(QString::fromUtf8(zName), bool(flags & SQLITE_OPEN_DELETEONCLOSE)); + if (!file->pFile->open(mode)) + return SQLITE_CANTOPEN; + if (pOutFlags) + *pOutFlags = flags; + + return SQLITE_OK; +} + +int xDelete(sqlite3_vfs *, const char *zName, int) +{ + return QFile::remove(QString::fromUtf8(zName)) ? SQLITE_OK : SQLITE_ERROR; +} + +int xAccess(sqlite3_vfs */*svfs*/, const char *zName, int flags, int *pResOut) +{ + *pResOut = 0; + switch (flags) { + case SQLITE_ACCESS_EXISTS: + case SQLITE_ACCESS_READ: + *pResOut = QFile::exists(QString::fromUtf8(zName)); + break; + default: + break; + } + return SQLITE_OK; +} + +int xFullPathname(sqlite3_vfs *, const char *zName, int nOut, char *zOut) +{ + if (!zName) + return SQLITE_ERROR; + + int i = 0; + for (;zName[i] && i < nOut; ++i) + zOut[i] = zName[i]; + + if (i >= nOut) + return SQLITE_ERROR; + + zOut[i] = '\0'; + return SQLITE_OK; +} + +int xRandomness(sqlite3_vfs *svfs, int nByte, char *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xRandomness(vfs, nByte, zOut); +} + +int xSleep(sqlite3_vfs *svfs, int microseconds) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xSleep(vfs, microseconds); +} + +int xCurrentTime(sqlite3_vfs *svfs, double *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTime(vfs, zOut); +} + +int xGetLastError(sqlite3_vfs *, int, char *) +{ + return 0; +} + +int xCurrentTimeInt64(sqlite3_vfs *svfs, sqlite3_int64 *zOut) +{ + auto vfs = static_cast<Vfs *>(svfs)->pVfs; + return vfs->xCurrentTimeInt64(vfs, zOut); +} +} // namespace { + +void register_qt_vfs() +{ + static Vfs vfs; + memset(&vfs, 0, sizeof(Vfs)); + vfs.iVersion = 1; + vfs.szOsFile = sizeof(File); + vfs.mxPathname = PATH_MAX; + vfs.zName = "QtVFS"; + vfs.xOpen = &xOpen; + vfs.xDelete = &xDelete; + vfs.xAccess = &xAccess; + vfs.xFullPathname = &xFullPathname; + vfs.xRandomness = &xRandomness; + vfs.xSleep = &xSleep; + vfs.xCurrentTime = &xCurrentTime; + vfs.xGetLastError = &xGetLastError; + vfs.xCurrentTimeInt64 = &xCurrentTimeInt64; + vfs.pVfs = sqlite3_vfs_find(nullptr); + vfs.ioMethods.iVersion = 1; + vfs.ioMethods.xClose = &xClose; + vfs.ioMethods.xRead = &xRead; + vfs.ioMethods.xWrite = &xWrite; + vfs.ioMethods.xTruncate = &xTruncate; + vfs.ioMethods.xSync = &xSync; + vfs.ioMethods.xFileSize = &xFileSize; + vfs.ioMethods.xLock = &xLock; + vfs.ioMethods.xUnlock = &xUnlock; + vfs.ioMethods.xCheckReservedLock = &xCheckReservedLock; + vfs.ioMethods.xFileControl = &xFileControl; + vfs.ioMethods.xSectorSize = &xSectorSize; + vfs.ioMethods.xDeviceCharacteristics = &xDeviceCharacteristics; + + sqlite3_vfs_register(&vfs, 0); +} diff --git a/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h new file mode 100644 index 0000000000..56024b3ecb --- /dev/null +++ b/src/plugins/sqldrivers/sqlite/qsql_sqlite_vfs_p.h @@ -0,0 +1,21 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QSQL_SQLITE_VFS_H +#define QSQL_SQLITE_VFS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +void register_qt_vfs(); + + +#endif // QSQL_SQLITE_VFS_H diff --git a/src/plugins/sqldrivers/sqlite/smain.cpp b/src/plugins/sqldrivers/sqlite/smain.cpp index f84a256bc8..0d201c38d3 100644 --- a/src/plugins/sqldrivers/sqlite/smain.cpp +++ b/src/plugins/sqldrivers/sqlite/smain.cpp @@ -4,6 +4,7 @@ #include <qsqldriverplugin.h> #include <qstringlist.h> #include "qsql_sqlite_p.h" +#include "qsql_sqlite_vfs_p.h" QT_BEGIN_NAMESPACE @@ -23,6 +24,7 @@ public: QSQLiteDriverPlugin::QSQLiteDriverPlugin() : QSqlDriverPlugin() { + register_qt_vfs(); } QSqlDriver* QSQLiteDriverPlugin::create(const QString &name) |